"""Tests for DMS Phase 3 — DOCUMENT CORE.

Covers:
  * DocumentType CRUD services + API
  * Document creation, update, archive, delete (service + API)
  * DocumentVersion: add, publish, rollback (service + API)
  * Version numbering (minor/major bump)
  * Status transitions
  * Duplicate code uniqueness guard
  * Cross-tenant isolation
"""

from __future__ import annotations

import io

import pytest

from simorgh.apps.dms.common.exceptions import AssetNotFound
from simorgh.apps.dms.documents import queries, services
from simorgh.apps.dms.documents.models import (
    Document,
    DocumentStatus,
    DocumentType,
    DocumentVersion,
    DocumentVersionStatus,
)
from simorgh.apps.dms.documents.permissions import (
    PERM_DOCTYPE_MANAGE,
    PERM_DOCTYPE_VIEW,
    PERM_DOCUMENT_CREATE,
    PERM_DOCUMENT_MANAGE,
    PERM_DOCUMENT_PUBLISH,
    PERM_DOCUMENT_VIEW,
)
from simorgh.apps.dms.documents.services import DocumentError, DocumentTypeError, VersionError
from simorgh.apps.dms.repositories.models import Repository
from simorgh.apps.iam.registry import sync_registry_to_db
from simorgh.core.context import RequestContext, use_request_context


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

ALL_DOC_PERMS = frozenset(
    {
        PERM_DOCTYPE_VIEW,
        PERM_DOCTYPE_MANAGE,
        PERM_DOCUMENT_VIEW,
        PERM_DOCUMENT_CREATE,
        PERM_DOCUMENT_MANAGE,
        PERM_DOCUMENT_PUBLISH,
    }
)


@pytest.fixture
def doc_ctx(alice, tenant_acme, acme_tree):
    return RequestContext(
        actor=alice,
        tenant=tenant_acme,
        org_node_ids=frozenset({acme_tree["root"].pk}),
        permissions=ALL_DOC_PERMS,
    )


@pytest.fixture
def alice_membership(alice, tenant_acme, acme_tree, role_admin):
    from simorgh.apps.memberships.models import Membership

    m = Membership.objects.create(
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
        role=role_admin,
    )
    m.users.add(alice)
    return m


@pytest.fixture
def repo(tenant_acme, acme_tree):
    return Repository.objects.create(
        name="Main Repo",
        slug="main-repo",
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
    )


@pytest.fixture
def doc_type(tenant_acme, acme_tree):
    return DocumentType.objects.create(
        name="Contract",
        code="contract",
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
    )


@pytest.fixture
def doc(repo, tenant_acme, acme_tree):
    return Document.objects.create(
        title="Test Document",
        repository=repo,
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
    )


@pytest.fixture
def file_asset(tenant_acme, acme_tree):
    """A ready FileMetadata asset for attaching to versions."""
    from simorgh.apps.storage.models import FileMetadata, FileUploadStatus
    from simorgh.apps.dms.assets.constants import APP_CONTEXT

    return FileMetadata.objects.create(
        filename="contract.pdf",
        content_type="application/pdf",
        size_bytes=1024,
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
        upload_status=FileUploadStatus.READY,
        app_context=APP_CONTEXT,
    )


# ---------------------------------------------------------------------------
# Service layer — DocumentType
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_create_document_type(tenant_acme, acme_tree):
    dt = services.create_document_type(
        name="Invoice",
        code="invoice",
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
        description="All invoices",
    )
    assert dt.pk is not None
    assert dt.code == "invoice"
    assert dt.is_active is True


@pytest.mark.django_db
def test_create_document_type_duplicate_code_raises(tenant_acme, acme_tree, doc_type):
    with pytest.raises(DocumentTypeError, match="already exists"):
        services.create_document_type(
            name="Another Contract",
            code="contract",
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
        )


@pytest.mark.django_db
def test_update_document_type(doc_type):
    services.update_document_type(doc_type, name="Updated Contract", is_active=False)
    doc_type.refresh_from_db()
    assert doc_type.name == "Updated Contract"
    assert doc_type.is_active is False


@pytest.mark.django_db
def test_delete_document_type(doc_type):
    services.delete_document_type(doc_type)
    assert not DocumentType.objects.filter(pk=doc_type.pk, is_deleted=False).exists()


# ---------------------------------------------------------------------------
# Service layer — Document
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_create_document(repo, tenant_acme, acme_tree):
    doc = services.create_document(
        title="NDA Agreement",
        repository=repo,
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
    )
    assert doc.pk is not None
    assert doc.status == DocumentStatus.DRAFT
    assert doc.current_version is None


@pytest.mark.django_db
def test_create_document_duplicate_code_raises(repo, tenant_acme, acme_tree):
    services.create_document(
        title="Doc A",
        repository=repo,
        code="DOC-001",
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
    )
    with pytest.raises(DocumentError, match="already exists"):
        services.create_document(
            title="Doc B",
            repository=repo,
            code="DOC-001",
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
        )


@pytest.mark.django_db
def test_update_document(doc):
    services.update_document(doc, title="Updated Title")
    doc.refresh_from_db()
    assert doc.title == "Updated Title"


@pytest.mark.django_db
def test_update_archived_document_raises(doc, tenant_acme, acme_tree):
    services.archive_document(doc)
    with pytest.raises(DocumentError, match="archived"):
        services.update_document(doc, title="New Title")


@pytest.mark.django_db
def test_archive_document(doc):
    services.archive_document(doc)
    doc.refresh_from_db()
    assert doc.status == DocumentStatus.ARCHIVED


@pytest.mark.django_db
def test_delete_document_soft(doc, tenant_acme, acme_tree):
    services.delete_document(doc)
    assert not Document.objects.filter(pk=doc.pk, is_deleted=False).exists()
    assert Document.objects.with_deleted().filter(pk=doc.pk).exists()


# ---------------------------------------------------------------------------
# Service layer — Versioning
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_add_first_version(doc, file_asset, tenant_acme, acme_tree):
    v = services.add_version(
        document=doc,
        file_asset=file_asset,
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
    )
    assert v.version_major == 1
    assert v.version_minor == 0
    assert v.status == DocumentVersionStatus.DRAFT
    assert v.is_current is True

    doc.refresh_from_db()
    assert doc.current_version_id == v.pk


@pytest.mark.django_db
def test_add_minor_version_bump(doc, file_asset, tenant_acme, acme_tree):
    v1 = services.add_version(
        document=doc, file_asset=file_asset,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    v2 = services.add_version(
        document=doc, file_asset=file_asset, bump="minor",
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    assert v2.version_major == 1
    assert v2.version_minor == 1
    v1.refresh_from_db()
    assert v1.is_current is False


@pytest.mark.django_db
def test_add_major_version_bump(doc, file_asset, tenant_acme, acme_tree):
    services.add_version(
        document=doc, file_asset=file_asset,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    v2 = services.add_version(
        document=doc, file_asset=file_asset, bump="major",
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    assert v2.version_major == 2
    assert v2.version_minor == 0


@pytest.mark.django_db
def test_publish_version(doc, file_asset, tenant_acme, acme_tree):
    v = services.add_version(
        document=doc, file_asset=file_asset,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    services.publish_version(v)
    v.refresh_from_db()
    doc.refresh_from_db()
    assert v.status == DocumentVersionStatus.PUBLISHED
    assert doc.status == DocumentStatus.PUBLISHED
    assert doc.current_version_id == v.pk


@pytest.mark.django_db
def test_publish_supersedes_previous_published(doc, file_asset, tenant_acme, acme_tree):
    v1 = services.add_version(
        document=doc, file_asset=file_asset,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    services.publish_version(v1)
    v2 = services.add_version(
        document=doc, file_asset=file_asset, bump="minor",
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    services.publish_version(v2)
    v1.refresh_from_db()
    assert v1.status == DocumentVersionStatus.SUPERSEDED


@pytest.mark.django_db
def test_publish_non_draft_raises(doc, file_asset, tenant_acme, acme_tree):
    v = services.add_version(
        document=doc, file_asset=file_asset,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    services.publish_version(v)
    with pytest.raises(VersionError, match="DRAFT"):
        services.publish_version(v)


@pytest.mark.django_db
def test_add_version_to_archived_doc_raises(doc, file_asset, tenant_acme, acme_tree):
    services.archive_document(doc)
    with pytest.raises(DocumentError, match="archived"):
        services.add_version(
            document=doc, file_asset=file_asset,
            tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
        )


@pytest.mark.django_db
def test_rollback_creates_new_version(doc, file_asset, tenant_acme, acme_tree):
    v1 = services.add_version(
        document=doc, file_asset=file_asset,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    services.add_version(
        document=doc, file_asset=file_asset, bump="minor",
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    rolled = services.rollback_to_version(
        document=doc, target_version=v1,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    assert rolled.version_major == 1
    assert rolled.version_minor == 2
    assert rolled.file_asset_id == file_asset.pk
    assert "Rollback" in rolled.label


@pytest.mark.django_db
def test_invalid_bump_raises(doc, file_asset, tenant_acme, acme_tree):
    with pytest.raises(VersionError, match="Invalid bump"):
        services.add_version(
            document=doc, file_asset=file_asset, bump="patch",
            tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
        )


# ---------------------------------------------------------------------------
# Query layer
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_get_document_not_found(tenant_acme):
    import uuid
    with pytest.raises(AssetNotFound):
        queries.get_document(tenant_acme.pk, str(uuid.uuid4()))


@pytest.mark.django_db
def test_list_documents_filters_by_status(repo, tenant_acme, acme_tree):
    d1 = services.create_document(
        title="Draft Doc", repository=repo,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    d2 = services.create_document(
        title="Published Doc", repository=repo,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    from simorgh.apps.storage.models import FileMetadata, FileUploadStatus
    from simorgh.apps.dms.assets.constants import APP_CONTEXT
    fa = FileMetadata.objects.create(
        filename="f.pdf", content_type="application/pdf", size_bytes=100,
        tenant=tenant_acme, organization_node=acme_tree["root"],
        upload_status=FileUploadStatus.READY, app_context=APP_CONTEXT,
    )
    v = services.add_version(
        document=d2, file_asset=fa,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    services.publish_version(v)

    published_qs = queries.list_documents(tenant_id=tenant_acme.pk, status=DocumentStatus.PUBLISHED)
    pks = list(published_qs.values_list("pk", flat=True))
    assert d2.pk in pks
    assert d1.pk not in pks


# ---------------------------------------------------------------------------
# HTTP API — Document types
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_api_create_document_type(client, alice, tenant_acme, acme_tree, alice_membership, doc_ctx):
    client.force_login(alice)
    with use_request_context(doc_ctx):
        resp = client.post(
            "/api/v1/dms/document-types/",
            data={"name": "Policy", "code": "policy"},
            content_type="application/json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 201
    assert resp.json()["code"] == "policy"


@pytest.mark.django_db
def test_api_list_document_types(client, alice, tenant_acme, acme_tree, alice_membership, doc_type, doc_ctx):
    client.force_login(alice)
    with use_request_context(doc_ctx):
        resp = client.get("/api/v1/dms/document-types/", HTTP_X_TENANT=tenant_acme.slug)
    assert resp.status_code == 200
    assert any(dt["code"] == "contract" for dt in resp.json())


@pytest.mark.django_db
def test_api_update_document_type(client, alice, tenant_acme, acme_tree, alice_membership, doc_type, doc_ctx):
    client.force_login(alice)
    with use_request_context(doc_ctx):
        resp = client.patch(
            f"/api/v1/dms/document-types/{doc_type.public_id}/",
            data={"name": "Legal Contract"},
            content_type="application/json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 200
    assert resp.json()["name"] == "Legal Contract"


@pytest.mark.django_db
def test_api_delete_document_type(client, alice, tenant_acme, acme_tree, alice_membership, doc_type, doc_ctx):
    client.force_login(alice)
    with use_request_context(doc_ctx):
        resp = client.delete(
            f"/api/v1/dms/document-types/{doc_type.public_id}/",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 204


# ---------------------------------------------------------------------------
# HTTP API — Documents
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_api_create_document(client, alice, tenant_acme, acme_tree, alice_membership, repo, doc_ctx):
    client.force_login(alice)
    with use_request_context(doc_ctx):
        resp = client.post(
            "/api/v1/dms/documents/",
            data={"title": "My Agreement", "repository_id": str(repo.public_id)},
            content_type="application/json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 201
    data = resp.json()
    assert data["title"] == "My Agreement"
    assert data["status"] == "draft"
    assert data["current_version"] is None


@pytest.mark.django_db
def test_api_get_document(client, alice, tenant_acme, acme_tree, alice_membership, doc, doc_ctx):
    client.force_login(alice)
    with use_request_context(doc_ctx):
        resp = client.get(
            f"/api/v1/dms/documents/{doc.public_id}/",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 200
    assert resp.json()["title"] == "Test Document"


@pytest.mark.django_db
def test_api_update_document(client, alice, tenant_acme, acme_tree, alice_membership, doc, doc_ctx):
    client.force_login(alice)
    with use_request_context(doc_ctx):
        resp = client.patch(
            f"/api/v1/dms/documents/{doc.public_id}/",
            data={"title": "Renamed Document"},
            content_type="application/json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 200
    assert resp.json()["title"] == "Renamed Document"


@pytest.mark.django_db
def test_api_delete_document(client, alice, tenant_acme, acme_tree, alice_membership, doc, doc_ctx):
    client.force_login(alice)
    with use_request_context(doc_ctx):
        resp = client.delete(
            f"/api/v1/dms/documents/{doc.public_id}/",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 204
    assert not Document.objects.filter(pk=doc.pk, is_deleted=False).exists()


@pytest.mark.django_db
def test_api_archive_document(client, alice, tenant_acme, acme_tree, alice_membership, doc, doc_ctx):
    client.force_login(alice)
    with use_request_context(doc_ctx):
        resp = client.post(
            f"/api/v1/dms/documents/{doc.public_id}/archive/",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 200
    assert resp.json()["status"] == "archived"


# ---------------------------------------------------------------------------
# HTTP API — Versions
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_api_add_version(client, alice, tenant_acme, acme_tree, alice_membership, doc, file_asset, doc_ctx):
    client.force_login(alice)
    with use_request_context(doc_ctx):
        resp = client.post(
            f"/api/v1/dms/documents/{doc.public_id}/versions/",
            data={"file_asset_id": str(file_asset.public_id), "bump": "minor"},
            content_type="application/json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 201
    data = resp.json()
    assert data["version_major"] == 1
    assert data["version_minor"] == 0
    assert data["status"] == "draft"


@pytest.mark.django_db
def test_api_publish_version(client, alice, tenant_acme, acme_tree, alice_membership, doc, file_asset, doc_ctx):
    v = services.add_version(
        document=doc, file_asset=file_asset,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    client.force_login(alice)
    with use_request_context(doc_ctx):
        resp = client.post(
            f"/api/v1/dms/documents/{doc.public_id}/versions/{v.public_id}/publish/",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 200
    assert resp.json()["status"] == "published"


@pytest.mark.django_db
def test_api_list_versions(client, alice, tenant_acme, acme_tree, alice_membership, doc, file_asset, doc_ctx):
    services.add_version(
        document=doc, file_asset=file_asset,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    services.add_version(
        document=doc, file_asset=file_asset, bump="minor",
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    client.force_login(alice)
    with use_request_context(doc_ctx):
        resp = client.get(
            f"/api/v1/dms/documents/{doc.public_id}/versions/",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 200
    assert len(resp.json()) == 2


@pytest.mark.django_db
def test_api_rollback_version(client, alice, tenant_acme, acme_tree, alice_membership, doc, file_asset, doc_ctx):
    v1 = services.add_version(
        document=doc, file_asset=file_asset,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    services.add_version(
        document=doc, file_asset=file_asset, bump="minor",
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    client.force_login(alice)
    with use_request_context(doc_ctx):
        resp = client.post(
            f"/api/v1/dms/documents/{doc.public_id}/versions/{v1.public_id}/rollback/",
            data={"change_summary": "Reverted"},
            content_type="application/json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 201
    data = resp.json()
    assert data["version_minor"] == 2  # 1.0 → 1.1 → 1.2 (rollback)
    assert "Rollback" in data["label"]
