"""Tests for DMS Phase 6 — VERSION OPERATIONS.

Covers:
  * checkout_document service — basic checkout, duplicate guard, conflict guard
  * checkin_document service — release with and without version, token mismatch
  * force_release_lock service — admin force release
  * release_expired_locks service — bulk expiry
  * request_version_diff / update_version_diff services
  * Queries: get_active_lock, list_checkout_history, list_version_diffs_for_document
  * HTTP API: checkout, checkin, lock status, force release, history, diff endpoints
  * Permission gates: 401 unauthenticated, 403 no permission, 409 conflict
"""

from __future__ import annotations

import uuid
from datetime import timedelta

import pytest
from django.utils import timezone

from simorgh.apps.dms.documents.services import add_version, publish_version
from simorgh.apps.dms.repositories.models import Repository
from simorgh.apps.dms.versioning import queries, services
from simorgh.apps.dms.versioning.iam_permissions import (
    PERM_VERSIONING_CHECKIN,
    PERM_VERSIONING_CHECKOUT,
    PERM_VERSIONING_COMPARE,
    PERM_VERSIONING_RELEASE,
)
from simorgh.apps.dms.versioning.models import (
    CheckoutAction,
    DiffStatus,
    DiffType,
    DocumentLock,
    LockStatus,
    VersionDiff,
)
from simorgh.apps.dms.versioning.services import LockConflictError, LockError, VersioningError
from simorgh.core.context import RequestContext, use_request_context

ALL_PERMS = frozenset({
    PERM_VERSIONING_CHECKOUT,
    PERM_VERSIONING_CHECKIN,
    PERM_VERSIONING_RELEASE,
    PERM_VERSIONING_COMPARE,
})


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@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 bob(db):
    from django.contrib.auth import get_user_model

    User = get_user_model()
    return User.objects.create_user(
        "+989000000099", password="x", email="bob@example.com",
        is_superuser=True,
    )


@pytest.fixture
def ver_ctx(alice, tenant_acme, acme_tree):
    return RequestContext(
        actor=alice,
        tenant=tenant_acme,
        org_node_ids=frozenset({acme_tree["root"].pk}),
        permissions=ALL_PERMS,
    )


@pytest.fixture
def repo(tenant_acme, acme_tree):
    return Repository.objects.create(
        name="Versioning Repo",
        slug="versioning-repo",
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
    )


@pytest.fixture
def doc(repo, tenant_acme, acme_tree):
    from simorgh.apps.dms.documents.models import Document

    return Document.objects.create(
        title="Versioned Document",
        repository=repo,
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
    )


@pytest.fixture
def file_asset(tenant_acme, acme_tree):
    from simorgh.apps.dms.assets.constants import APP_CONTEXT
    from simorgh.apps.storage.models import FileMetadata, FileUploadStatus

    return FileMetadata.objects.create(
        filename="v1.pdf",
        content_type="application/pdf",
        size_bytes=1024,
        path=f"test/phase6/{uuid.uuid4()}.pdf",
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
        upload_status=FileUploadStatus.READY,
        app_context=APP_CONTEXT,
    )


@pytest.fixture
def file_asset2(tenant_acme, acme_tree):
    from simorgh.apps.dms.assets.constants import APP_CONTEXT
    from simorgh.apps.storage.models import FileMetadata, FileUploadStatus

    return FileMetadata.objects.create(
        filename="v2.pdf",
        content_type="application/pdf",
        size_bytes=2048,
        path=f"test/phase6/{uuid.uuid4()}.pdf",
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
        upload_status=FileUploadStatus.READY,
        app_context=APP_CONTEXT,
    )


@pytest.fixture
def v1(doc, file_asset, tenant_acme, acme_tree):
    v = add_version(
        document=doc,
        file_asset=file_asset,
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
    )
    publish_version(v)
    return v


@pytest.fixture
def v2(doc, file_asset2, v1, tenant_acme, acme_tree):
    return add_version(
        document=doc,
        file_asset=file_asset2,
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
    )


@pytest.fixture
def lock(doc, alice, tenant_acme, acme_tree):
    return services.checkout_document(
        doc,
        user=alice,
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
    )


# ---------------------------------------------------------------------------
# Service — checkout
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_checkout_creates_lock(doc, alice, tenant_acme, acme_tree):
    lock = services.checkout_document(
        doc,
        user=alice,
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
        notes="Starting work",
    )
    assert lock.pk is not None
    assert lock.is_active is True
    assert lock.status == LockStatus.ACTIVE
    assert lock.locked_by_id == alice.pk
    assert lock.notes == "Starting work"
    assert lock.lock_token is not None


@pytest.mark.django_db
def test_checkout_same_user_raises_lock_error(doc, alice, tenant_acme, acme_tree, lock):
    with pytest.raises(LockError, match="already have this document checked out"):
        services.checkout_document(
            doc,
            user=alice,
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
        )


@pytest.mark.django_db
def test_checkout_by_another_user_raises_conflict(
    doc, bob, tenant_acme, acme_tree, lock
):
    with pytest.raises(LockConflictError, match="already checked out"):
        services.checkout_document(
            doc,
            user=bob,
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
        )


@pytest.mark.django_db
def test_checkout_archived_document_raises(doc, alice, tenant_acme, acme_tree):
    from simorgh.apps.dms.documents.services import archive_document, DocumentError

    archive_document(doc)
    with pytest.raises(DocumentError, match="archived"):
        services.checkout_document(
            doc,
            user=alice,
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
        )


@pytest.mark.django_db
def test_checkout_creates_audit_event(doc, alice, tenant_acme, acme_tree, lock):
    events = list(queries.list_checkout_history(doc))
    assert len(events) == 1
    assert events[0].action == CheckoutAction.CHECKOUT
    assert events[0].actor_id == alice.pk
    assert events[0].lock_token == lock.lock_token


# ---------------------------------------------------------------------------
# Service — checkin
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_checkin_releases_lock(lock, alice, tenant_acme, acme_tree):
    lock, version = services.checkin_document(lock)
    assert lock.is_active is False
    assert lock.status == LockStatus.RELEASED
    assert version is None


@pytest.mark.django_db
def test_checkin_with_file_creates_version(lock, file_asset2, doc):
    lock, version = services.checkin_document(lock, file_asset=file_asset2)
    assert version is not None
    assert version.file_asset_id == file_asset2.pk
    assert version.document_id == doc.pk


@pytest.mark.django_db
def test_checkin_token_mismatch_raises(lock):
    with pytest.raises(LockError, match="token mismatch"):
        services.checkin_document(lock, lock_token=uuid.uuid4())


@pytest.mark.django_db
def test_checkin_correct_token_succeeds(lock):
    lock, _ = services.checkin_document(lock, lock_token=lock.lock_token)
    assert lock.status == LockStatus.RELEASED


@pytest.mark.django_db
def test_checkin_creates_audit_event(lock, file_asset2):
    services.checkin_document(lock, file_asset=file_asset2)
    events = list(queries.list_checkout_history(lock.document))
    actions = [e.action for e in events]
    assert CheckoutAction.CHECKIN in actions


@pytest.mark.django_db
def test_checkin_inactive_lock_raises(lock):
    services.checkin_document(lock)
    with pytest.raises(LockError, match="no longer active"):
        services.checkin_document(lock)


# ---------------------------------------------------------------------------
# Service — force release
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_force_release_lock(doc, alice, bob, tenant_acme, acme_tree, lock):
    lock = services.force_release_lock(lock, released_by=bob, reason="Emergency")
    assert lock.is_active is False
    assert lock.status == LockStatus.FORCE_RELEASED

    events = list(queries.list_checkout_history(doc))
    force_ev = next(e for e in events if e.action == CheckoutAction.FORCE_RELEASE)
    assert force_ev.actor_id == bob.pk
    assert force_ev.lock_holder_id == alice.pk
    assert force_ev.notes == "Emergency"


@pytest.mark.django_db
def test_force_release_inactive_lock_raises(lock):
    services.force_release_lock(lock, released_by=lock.locked_by)
    with pytest.raises(LockError, match="already inactive"):
        services.force_release_lock(lock, released_by=lock.locked_by)


# ---------------------------------------------------------------------------
# Service — release expired locks
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_release_expired_locks(doc, alice, tenant_acme, acme_tree):
    lock = services.checkout_document(
        doc,
        user=alice,
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
        expires_at=timezone.now() - timedelta(minutes=1),
    )
    released = services.release_expired_locks()
    assert any(l.pk == lock.pk for l in released)
    lock.refresh_from_db()
    assert lock.is_active is False
    assert lock.status == LockStatus.EXPIRED


@pytest.mark.django_db
def test_release_expired_locks_skips_non_expired(doc, alice, tenant_acme, acme_tree):
    services.checkout_document(
        doc,
        user=alice,
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
        expires_at=timezone.now() + timedelta(hours=1),  # future
    )
    released = services.release_expired_locks()
    assert len(released) == 0


# ---------------------------------------------------------------------------
# Service — version diff
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_request_version_diff_creates_pending(v1, v2, tenant_acme, acme_tree):
    diff = services.request_version_diff(
        v1, v2,
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
    )
    assert diff.pk is not None
    assert diff.status == DiffStatus.PENDING
    assert diff.diff_type == DiffType.FULL


@pytest.mark.django_db
def test_request_version_diff_idempotent(v1, v2, tenant_acme, acme_tree):
    diff1 = services.request_version_diff(
        v1, v2,
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
    )
    diff2 = services.request_version_diff(
        v1, v2,
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
    )
    assert diff1.pk == diff2.pk


@pytest.mark.django_db
def test_request_diff_same_version_raises(v1, tenant_acme, acme_tree):
    with pytest.raises(VersioningError, match="itself"):
        services.request_version_diff(
            v1, v1,
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
        )


@pytest.mark.django_db
def test_update_version_diff(v1, v2, tenant_acme, acme_tree):
    diff = services.request_version_diff(
        v1, v2,
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
    )
    diff = services.update_version_diff(
        diff,
        diff_summary="2 paragraphs changed",
        diff_data={"added": 5, "removed": 3},
        status=DiffStatus.READY,
    )
    assert diff.status == DiffStatus.READY
    assert diff.diff_summary == "2 paragraphs changed"
    assert diff.diff_data["added"] == 5


@pytest.mark.django_db
def test_update_diff_invalid_status_raises(v1, v2, tenant_acme, acme_tree):
    diff = services.request_version_diff(
        v1, v2,
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
    )
    with pytest.raises(VersioningError, match="Invalid diff status"):
        services.update_version_diff(diff, status="unknown")


# ---------------------------------------------------------------------------
# Queries
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_get_active_lock_returns_none_when_not_locked(doc):
    assert queries.get_active_lock(doc) is None


@pytest.mark.django_db
def test_get_active_lock_returns_lock(doc, lock):
    found = queries.get_active_lock(doc)
    assert found is not None
    assert found.pk == lock.pk


@pytest.mark.django_db
def test_list_checkout_history_ordered_newest_first(doc, alice, tenant_acme, acme_tree):
    lock = services.checkout_document(
        doc,
        user=alice,
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
    )
    services.checkin_document(lock)
    events = list(queries.list_checkout_history(doc))
    actions = {e.action for e in events}
    assert CheckoutAction.CHECKIN in actions
    assert CheckoutAction.CHECKOUT in actions
    assert len(events) == 2


@pytest.mark.django_db
def test_list_version_diffs_for_document(doc, v1, v2, tenant_acme, acme_tree):
    services.request_version_diff(
        v1, v2,
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
    )
    diffs = list(queries.list_version_diffs_for_document(doc))
    assert len(diffs) == 1


# ---------------------------------------------------------------------------
# HTTP API — lock status
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_api_lock_status_not_locked(client, alice, tenant_acme, acme_tree,
                                     alice_membership, doc, ver_ctx):
    client.force_login(alice)
    with use_request_context(ver_ctx):
        resp = client.get(
            f"/api/v1/dms/documents/{doc.public_id}/lock/",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 200
    assert resp.json()["locked"] is False


@pytest.mark.django_db
def test_api_lock_status_locked(client, alice, tenant_acme, acme_tree,
                                 alice_membership, doc, lock, ver_ctx):
    client.force_login(alice)
    with use_request_context(ver_ctx):
        resp = client.get(
            f"/api/v1/dms/documents/{doc.public_id}/lock/",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 200
    data = resp.json()
    assert data["locked"] is True
    assert data["lock"]["status"] == "active"


# ---------------------------------------------------------------------------
# HTTP API — checkout
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_api_checkout(client, alice, tenant_acme, acme_tree,
                       alice_membership, doc, ver_ctx):
    client.force_login(alice)
    with use_request_context(ver_ctx):
        resp = client.post(
            f"/api/v1/dms/documents/{doc.public_id}/checkout/",
            data={"notes": "Working on section 2"},
            content_type="application/json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 201
    data = resp.json()
    assert data["status"] == "active"
    assert data["is_active"] is True
    assert "lock_token" in data


@pytest.mark.django_db
def test_api_checkout_conflict(client, bob, tenant_acme, acme_tree,
                                alice_membership, doc, lock, ver_ctx):
    # bob tries to check out a document already locked by alice
    bob_ctx = RequestContext(
        actor=bob,
        tenant=tenant_acme,
        org_node_ids=frozenset({acme_tree["root"].pk}),
        permissions=ALL_PERMS,
    )
    client.force_login(bob)
    with use_request_context(bob_ctx):
        resp = client.post(
            f"/api/v1/dms/documents/{doc.public_id}/checkout/",
            content_type="application/json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 409


# ---------------------------------------------------------------------------
# HTTP API — checkin
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_api_checkin(client, alice, tenant_acme, acme_tree,
                      alice_membership, doc, lock, ver_ctx):
    client.force_login(alice)
    with use_request_context(ver_ctx):
        resp = client.post(
            f"/api/v1/dms/documents/{doc.public_id}/checkin/",
            data={"change_summary": "Done editing", "lock_token": str(lock.lock_token)},
            content_type="application/json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 200
    assert resp.json()["status"] == "released"
    assert resp.json()["version_created"] is None


@pytest.mark.django_db
def test_api_checkin_not_checked_out(client, alice, tenant_acme, acme_tree,
                                      alice_membership, doc, ver_ctx):
    client.force_login(alice)
    with use_request_context(ver_ctx):
        resp = client.post(
            f"/api/v1/dms/documents/{doc.public_id}/checkin/",
            content_type="application/json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 400


# ---------------------------------------------------------------------------
# HTTP API — force release
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_api_force_release(client, alice, tenant_acme, acme_tree,
                            alice_membership, doc, lock, ver_ctx):
    client.force_login(alice)
    with use_request_context(ver_ctx):
        resp = client.post(
            f"/api/v1/dms/documents/{doc.public_id}/release-lock/",
            data={"reason": "Admin override"},
            content_type="application/json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 200
    assert resp.json()["status"] == "force_released"


@pytest.mark.django_db
def test_api_force_release_no_lock(client, alice, tenant_acme, acme_tree,
                                    alice_membership, doc, ver_ctx):
    client.force_login(alice)
    with use_request_context(ver_ctx):
        resp = client.post(
            f"/api/v1/dms/documents/{doc.public_id}/release-lock/",
            content_type="application/json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 400


# ---------------------------------------------------------------------------
# HTTP API — checkout history
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_api_checkout_history(client, alice, tenant_acme, acme_tree,
                               alice_membership, doc, lock, ver_ctx):
    services.checkin_document(lock)
    client.force_login(alice)
    with use_request_context(ver_ctx):
        resp = client.get(
            f"/api/v1/dms/documents/{doc.public_id}/checkout-history/",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 200
    events = resp.json()
    assert len(events) == 2
    actions = [e["action"] for e in events]
    assert "checkin" in actions
    assert "checkout" in actions


# ---------------------------------------------------------------------------
# HTTP API — version diff
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_api_request_diff(client, alice, tenant_acme, acme_tree,
                           alice_membership, doc, v1, v2, ver_ctx):
    client.force_login(alice)
    with use_request_context(ver_ctx):
        resp = client.post(
            f"/api/v1/dms/documents/{doc.public_id}/versions/diff/",
            data={
                "from_version_id": str(v1.public_id),
                "to_version_id": str(v2.public_id),
                "diff_type": "full",
            },
            content_type="application/json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 201
    data = resp.json()
    assert data["status"] == "pending"
    assert data["diff_type"] == "full"


@pytest.mark.django_db
def test_api_list_diffs(client, alice, tenant_acme, acme_tree,
                         alice_membership, doc, v1, v2, ver_ctx):
    services.request_version_diff(
        v1, v2,
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
    )
    client.force_login(alice)
    with use_request_context(ver_ctx):
        resp = client.get(
            f"/api/v1/dms/documents/{doc.public_id}/versions/diff/",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 200
    assert len(resp.json()) == 1


@pytest.mark.django_db
def test_api_diff_detail(client, alice, tenant_acme, acme_tree,
                          alice_membership, doc, v1, v2, ver_ctx):
    diff = services.request_version_diff(
        v1, v2,
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
    )
    client.force_login(alice)
    with use_request_context(ver_ctx):
        resp = client.get(
            f"/api/v1/dms/documents/{doc.public_id}/versions/diff/{diff.public_id}/",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 200
    assert str(resp.json()["public_id"]) == str(diff.public_id)


@pytest.mark.django_db
def test_api_diff_same_version_returns_400(client, alice, tenant_acme, acme_tree,
                                            alice_membership, doc, v1, ver_ctx):
    client.force_login(alice)
    with use_request_context(ver_ctx):
        resp = client.post(
            f"/api/v1/dms/documents/{doc.public_id}/versions/diff/",
            data={
                "from_version_id": str(v1.public_id),
                "to_version_id": str(v1.public_id),
            },
            content_type="application/json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 400


# ---------------------------------------------------------------------------
# Permission gates
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_api_checkout_requires_auth(client, tenant_acme, doc):
    resp = client.post(
        f"/api/v1/dms/documents/{doc.public_id}/checkout/",
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 401


@pytest.mark.django_db
def test_api_checkout_requires_perm(client, alice, tenant_acme, acme_tree, doc):
    # alice has no membership → middleware gives empty permissions → 403
    client.force_login(alice)
    resp = client.post(
        f"/api/v1/dms/documents/{doc.public_id}/checkout/",
        content_type="application/json",
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 403


@pytest.mark.django_db
def test_api_force_release_requires_release_perm(client, alice, tenant_acme, acme_tree, doc):
    # alice has no membership → middleware gives empty permissions → 403
    client.force_login(alice)
    resp = client.post(
        f"/api/v1/dms/documents/{doc.public_id}/release-lock/",
        content_type="application/json",
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 403
