"""Tests for DMS Phase 12 — Audit & Observability.

Covers:
  * log_document_action — creates immutable DocumentAuditLog
  * log_access — creates immutable AccessLog
  * log_security_event — creates immutable SecurityEvent
  * Queries: list_document_audit_logs (with action filter)
  * Queries: get_audit_log, not-found raises AssetNotFound
  * Queries: list_access_logs (with access_type filter)
  * Queries: list_security_events (tenant-wide, doc-scoped, with filters)
  * Queries: get_security_event, not-found raises AssetNotFound
  * HTTP API: GET /audit/ list + action filter
  * HTTP API: GET /audit/{id}/ detail + 404
  * HTTP API: GET /access-logs/ list + access_type filter
  * HTTP API: GET /security-events/ (doc-scoped)
  * HTTP API: GET /security-events/ (tenant-wide)
  * HTTP API: GET /security-events/{id}/ detail + 404
  * Permission gates: 401 unauthenticated, 403 no permission
"""

from __future__ import annotations

import uuid

import pytest

from simorgh.apps.dms.audit import queries, services
from simorgh.apps.dms.audit.iam_permissions import (
    PERM_ACCESS_LOG_VIEW,
    PERM_AUDIT_VIEW,
    PERM_SECURITY_EVENTS_VIEW,
)
from simorgh.apps.dms.audit.models import (
    AccessLog,
    AccessType,
    DocumentAction,
    DocumentAuditLog,
    SecurityEvent,
    SecurityEventSeverity,
    SecurityEventType,
)
from simorgh.apps.dms.common.exceptions import AssetNotFound
from simorgh.apps.dms.repositories.models import Repository


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def alice_membership(alice, tenant_acme, acme_tree, role_admin):
    """Override conftest alice_membership — alice gets role_admin (all perms)."""
    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 charlie(db):
    """Non-member user for 403 tests."""
    from django.contrib.auth import get_user_model

    User = get_user_model()
    return User.objects.create_user(
        "+989000000099",
        password="x",
        email="charlie_audit@example.com",
    )


@pytest.fixture
def repo(tenant_acme, acme_tree):
    return Repository.objects.create(
        name="Audit Test Repo",
        slug="audit-test-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="Audit Phase 12 Document",
        repository=repo,
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
    )


@pytest.fixture
def audit_entry(doc, alice, tenant_acme):
    return services.log_document_action(
        tenant_id=tenant_acme.pk,
        document_id=doc.pk,
        action=DocumentAction.CREATED,
        actor=alice,
        metadata={"source": "test"},
    )


@pytest.fixture
def access_entry(doc, alice, tenant_acme):
    return services.log_access(
        tenant_id=tenant_acme.pk,
        document_id=doc.pk,
        access_type=AccessType.VIEWED,
        actor=alice,
        ip_address="127.0.0.1",
    )


@pytest.fixture
def sec_event(doc, alice, tenant_acme):
    return services.log_security_event(
        tenant_id=tenant_acme.pk,
        event_type=SecurityEventType.PERMISSION_DENIED,
        severity=SecurityEventSeverity.WARNING,
        document_id=doc.pk,
        actor=alice,
        description="test perm denied",
    )


# ---------------------------------------------------------------------------
# URL helper  — uses document public_id in path
# ---------------------------------------------------------------------------

def _url(path: str) -> str:
    return f"/api/v1/dms/{path}"


# ---------------------------------------------------------------------------
# Service — log_document_action
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_log_document_action_creates_record(doc, alice, tenant_acme):
    entry = services.log_document_action(
        tenant_id=tenant_acme.pk,
        document_id=doc.pk,
        action=DocumentAction.PUBLISHED,
        actor=alice,
        before_state={"status": "draft"},
        after_state={"status": "published"},
        metadata={"comment": "first publish"},
    )
    assert entry.pk is not None
    assert entry.public_id is not None
    assert entry.action == DocumentAction.PUBLISHED
    assert entry.before_state == {"status": "draft"}
    assert entry.after_state == {"status": "published"}
    assert entry.actor_id == alice.pk
    assert entry.tenant_id == tenant_acme.pk
    assert entry.occurred_at is not None


@pytest.mark.django_db
def test_log_document_action_system_event(doc, tenant_acme):
    entry = services.log_document_action(
        tenant_id=tenant_acme.pk,
        document_id=doc.pk,
        action=DocumentAction.ARCHIVED,
        actor=None,
    )
    assert entry.actor is None
    assert entry.action == DocumentAction.ARCHIVED


@pytest.mark.django_db
def test_log_document_action_with_ip_and_ua(doc, alice, tenant_acme):
    entry = services.log_document_action(
        tenant_id=tenant_acme.pk,
        document_id=doc.pk,
        action=DocumentAction.VERSION_ADDED,
        actor=alice,
        ip_address="10.0.0.1",
        user_agent="Mozilla/5.0 TestRunner",
    )
    assert entry.ip_address == "10.0.0.1"
    assert entry.user_agent == "Mozilla/5.0 TestRunner"


@pytest.mark.django_db
def test_multiple_audit_entries_for_same_doc(doc, alice, tenant_acme):
    for action in [DocumentAction.CREATED, DocumentAction.METADATA_UPDATED, DocumentAction.PUBLISHED]:
        services.log_document_action(
            tenant_id=tenant_acme.pk,
            document_id=doc.pk,
            action=action,
            actor=alice,
        )
    count = DocumentAuditLog.objects.filter(document=doc).count()
    assert count == 3


# ---------------------------------------------------------------------------
# Service — log_access
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_log_access_creates_record(doc, alice, tenant_acme):
    entry = services.log_access(
        tenant_id=tenant_acme.pk,
        document_id=doc.pk,
        access_type=AccessType.DOWNLOADED,
        actor=alice,
        ip_address="192.168.1.10",
        metadata={"size_bytes": 4096},
    )
    assert entry.pk is not None
    assert entry.access_type == AccessType.DOWNLOADED
    assert entry.actor_id == alice.pk
    assert entry.ip_address == "192.168.1.10"
    assert entry.metadata["size_bytes"] == 4096


@pytest.mark.django_db
def test_log_access_anonymous(doc, tenant_acme):
    entry = services.log_access(
        tenant_id=tenant_acme.pk,
        document_id=doc.pk,
        access_type=AccessType.SHARE_LINK,
        actor=None,
    )
    assert entry.actor is None
    assert entry.access_type == AccessType.SHARE_LINK


# ---------------------------------------------------------------------------
# Service — log_security_event
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_log_security_event_with_document(doc, alice, tenant_acme):
    event = services.log_security_event(
        tenant_id=tenant_acme.pk,
        event_type=SecurityEventType.UNAUTHORIZED_ACCESS,
        severity=SecurityEventSeverity.CRITICAL,
        document_id=doc.pk,
        actor=alice,
        description="Access denied",
        ip_address="1.2.3.4",
        metadata={"permission": "dms.doc.publish"},
    )
    assert event.pk is not None
    assert event.event_type == SecurityEventType.UNAUTHORIZED_ACCESS
    assert event.severity == SecurityEventSeverity.CRITICAL
    assert event.document_id == doc.pk
    assert event.description == "Access denied"
    assert event.metadata["permission"] == "dms.doc.publish"


@pytest.mark.django_db
def test_log_security_event_tenant_level(tenant_acme):
    event = services.log_security_event(
        tenant_id=tenant_acme.pk,
        event_type=SecurityEventType.BULK_DOWNLOAD,
        severity=SecurityEventSeverity.WARNING,
        document_id=None,
    )
    assert event.document_id is None
    assert event.event_type == SecurityEventType.BULK_DOWNLOAD


# ---------------------------------------------------------------------------
# Queries — DocumentAuditLog
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_list_document_audit_logs(audit_entry, doc, tenant_acme):
    results = list(queries.list_document_audit_logs(
        tenant_id=tenant_acme.pk,
        document_id=doc.pk,
    ))
    assert any(e.pk == audit_entry.pk for e in results)


@pytest.mark.django_db
def test_list_document_audit_logs_filter_by_action(doc, alice, tenant_acme):
    services.log_document_action(
        tenant_id=tenant_acme.pk, document_id=doc.pk,
        action=DocumentAction.CREATED, actor=alice,
    )
    services.log_document_action(
        tenant_id=tenant_acme.pk, document_id=doc.pk,
        action=DocumentAction.PUBLISHED, actor=alice,
    )
    published = list(queries.list_document_audit_logs(
        tenant_id=tenant_acme.pk,
        document_id=doc.pk,
        action=DocumentAction.PUBLISHED,
    ))
    assert all(e.action == DocumentAction.PUBLISHED for e in published)
    assert len(published) == 1


@pytest.mark.django_db
def test_get_audit_log_returns_entry(audit_entry, tenant_acme):
    result = queries.get_audit_log(
        tenant_id=tenant_acme.pk, log_id=audit_entry.public_id
    )
    assert result.pk == audit_entry.pk


@pytest.mark.django_db
def test_get_audit_log_not_found_raises(tenant_acme):
    with pytest.raises(AssetNotFound):
        queries.get_audit_log(tenant_id=tenant_acme.pk, log_id=uuid.uuid4())


# ---------------------------------------------------------------------------
# Queries — AccessLog
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_list_access_logs(access_entry, doc, tenant_acme):
    results = list(queries.list_access_logs(
        tenant_id=tenant_acme.pk,
        document_id=doc.pk,
    ))
    assert any(e.pk == access_entry.pk for e in results)


@pytest.mark.django_db
def test_list_access_logs_filter_by_type(doc, alice, tenant_acme):
    services.log_access(
        tenant_id=tenant_acme.pk, document_id=doc.pk,
        access_type=AccessType.VIEWED, actor=alice,
    )
    services.log_access(
        tenant_id=tenant_acme.pk, document_id=doc.pk,
        access_type=AccessType.DOWNLOADED, actor=alice,
    )
    downloads = list(queries.list_access_logs(
        tenant_id=tenant_acme.pk,
        document_id=doc.pk,
        access_type=AccessType.DOWNLOADED,
    ))
    assert all(e.access_type == AccessType.DOWNLOADED for e in downloads)
    assert len(downloads) == 1


# ---------------------------------------------------------------------------
# Queries — SecurityEvent
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_list_security_events_tenant_wide(sec_event, tenant_acme):
    results = list(queries.list_security_events(tenant_id=tenant_acme.pk))
    assert any(e.pk == sec_event.pk for e in results)


@pytest.mark.django_db
def test_list_security_events_doc_scoped(sec_event, doc, tenant_acme):
    results = list(queries.list_security_events(
        tenant_id=tenant_acme.pk,
        document_id=doc.pk,
    ))
    assert any(e.pk == sec_event.pk for e in results)


@pytest.mark.django_db
def test_list_security_events_filter_severity(doc, tenant_acme):
    services.log_security_event(
        tenant_id=tenant_acme.pk, document_id=doc.pk,
        event_type=SecurityEventType.PERMISSION_DENIED,
        severity=SecurityEventSeverity.INFO,
    )
    services.log_security_event(
        tenant_id=tenant_acme.pk, document_id=doc.pk,
        event_type=SecurityEventType.UNAUTHORIZED_ACCESS,
        severity=SecurityEventSeverity.CRITICAL,
    )
    critical = list(queries.list_security_events(
        tenant_id=tenant_acme.pk,
        document_id=doc.pk,
        severity=SecurityEventSeverity.CRITICAL,
    ))
    assert all(e.severity == SecurityEventSeverity.CRITICAL for e in critical)
    assert len(critical) == 1


@pytest.mark.django_db
def test_get_security_event_returns_event(sec_event, tenant_acme):
    result = queries.get_security_event(
        tenant_id=tenant_acme.pk, event_id=sec_event.public_id
    )
    assert result.pk == sec_event.pk


@pytest.mark.django_db
def test_get_security_event_not_found_raises(tenant_acme):
    with pytest.raises(AssetNotFound):
        queries.get_security_event(tenant_id=tenant_acme.pk, event_id=uuid.uuid4())


# ---------------------------------------------------------------------------
# HTTP API — Document Audit Log
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_api_list_audit_logs(
    client, alice, alice_membership, doc, tenant_acme, audit_entry
):
    client.force_login(alice)
    resp = client.get(
        _url(f"documents/{doc.public_id}/audit/"),
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 200
    ids = [e["id"] for e in resp.json()]
    assert str(audit_entry.public_id) in ids


@pytest.mark.django_db
def test_api_list_audit_logs_filter_action(
    client, alice, alice_membership, doc, tenant_acme
):
    services.log_document_action(
        tenant_id=tenant_acme.pk, document_id=doc.pk,
        action=DocumentAction.CREATED, actor=alice,
    )
    services.log_document_action(
        tenant_id=tenant_acme.pk, document_id=doc.pk,
        action=DocumentAction.PUBLISHED, actor=alice,
    )
    client.force_login(alice)
    resp = client.get(
        _url(f"documents/{doc.public_id}/audit/"),
        {"action": DocumentAction.PUBLISHED},
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 200
    data = resp.json()
    assert len(data) == 1
    assert data[0]["action"] == DocumentAction.PUBLISHED


@pytest.mark.django_db
def test_api_get_audit_log_detail(
    client, alice, alice_membership, doc, tenant_acme, audit_entry
):
    client.force_login(alice)
    resp = client.get(
        _url(f"documents/{doc.public_id}/audit/{audit_entry.public_id}/"),
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 200
    assert resp.json()["id"] == str(audit_entry.public_id)
    assert resp.json()["action"] == DocumentAction.CREATED


@pytest.mark.django_db
def test_api_audit_log_not_found(
    client, alice, alice_membership, doc, tenant_acme
):
    client.force_login(alice)
    resp = client.get(
        _url(f"documents/{doc.public_id}/audit/{uuid.uuid4()}/"),
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 404


# ---------------------------------------------------------------------------
# HTTP API — Access Logs
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_api_list_access_logs(
    client, alice, alice_membership, doc, tenant_acme, access_entry
):
    client.force_login(alice)
    resp = client.get(
        _url(f"documents/{doc.public_id}/access-logs/"),
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 200
    ids = [e["id"] for e in resp.json()]
    assert str(access_entry.public_id) in ids


@pytest.mark.django_db
def test_api_list_access_logs_filter_type(
    client, alice, alice_membership, doc, tenant_acme
):
    services.log_access(
        tenant_id=tenant_acme.pk, document_id=doc.pk,
        access_type=AccessType.VIEWED, actor=alice,
    )
    services.log_access(
        tenant_id=tenant_acme.pk, document_id=doc.pk,
        access_type=AccessType.DOWNLOADED, actor=alice,
    )
    client.force_login(alice)
    resp = client.get(
        _url(f"documents/{doc.public_id}/access-logs/"),
        {"access_type": AccessType.DOWNLOADED},
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 200
    data = resp.json()
    assert len(data) == 1
    assert data[0]["access_type"] == AccessType.DOWNLOADED


# ---------------------------------------------------------------------------
# HTTP API — Security Events (document-scoped)
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_api_list_doc_security_events(
    client, alice, alice_membership, doc, tenant_acme, sec_event
):
    client.force_login(alice)
    resp = client.get(
        _url(f"documents/{doc.public_id}/security-events/"),
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 200
    ids = [e["id"] for e in resp.json()]
    assert str(sec_event.public_id) in ids


@pytest.mark.django_db
def test_api_list_doc_security_events_filter_severity(
    client, alice, alice_membership, doc, tenant_acme
):
    services.log_security_event(
        tenant_id=tenant_acme.pk, document_id=doc.pk,
        event_type=SecurityEventType.PERMISSION_DENIED,
        severity=SecurityEventSeverity.INFO,
    )
    services.log_security_event(
        tenant_id=tenant_acme.pk, document_id=doc.pk,
        event_type=SecurityEventType.UNAUTHORIZED_ACCESS,
        severity=SecurityEventSeverity.CRITICAL,
    )
    client.force_login(alice)
    resp = client.get(
        _url(f"documents/{doc.public_id}/security-events/"),
        {"severity": SecurityEventSeverity.CRITICAL},
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 200
    data = resp.json()
    assert all(e["severity"] == SecurityEventSeverity.CRITICAL for e in data)


# ---------------------------------------------------------------------------
# HTTP API — Security Events (tenant-wide)
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_api_list_tenant_security_events(
    client, alice, alice_membership, tenant_acme, sec_event
):
    client.force_login(alice)
    resp = client.get(
        _url("security-events/"),
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 200
    ids = [e["id"] for e in resp.json()]
    assert str(sec_event.public_id) in ids


@pytest.mark.django_db
def test_api_get_security_event_detail(
    client, alice, alice_membership, tenant_acme, sec_event
):
    client.force_login(alice)
    resp = client.get(
        _url(f"security-events/{sec_event.public_id}/"),
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 200
    assert resp.json()["id"] == str(sec_event.public_id)
    assert resp.json()["severity"] == SecurityEventSeverity.WARNING


@pytest.mark.django_db
def test_api_security_event_not_found(
    client, alice, alice_membership, tenant_acme
):
    client.force_login(alice)
    resp = client.get(
        _url(f"security-events/{uuid.uuid4()}/"),
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 404


# ---------------------------------------------------------------------------
# Permission gates — 401 unauthenticated
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_api_audit_requires_auth(client, doc, tenant_acme):
    resp = client.get(
        _url(f"documents/{doc.public_id}/audit/"),
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 401


@pytest.mark.django_db
def test_api_access_logs_requires_auth(client, doc, tenant_acme):
    resp = client.get(
        _url(f"documents/{doc.public_id}/access-logs/"),
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 401


@pytest.mark.django_db
def test_api_security_events_requires_auth(client, tenant_acme):
    resp = client.get(
        _url("security-events/"),
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 401


# ---------------------------------------------------------------------------
# Permission gates — 403 no permission (charlie has no membership)
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_api_audit_requires_perm(client, charlie, doc, tenant_acme):
    client.force_login(charlie)
    resp = client.get(
        _url(f"documents/{doc.public_id}/audit/"),
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 403


@pytest.mark.django_db
def test_api_access_logs_requires_perm(client, charlie, doc, tenant_acme):
    client.force_login(charlie)
    resp = client.get(
        _url(f"documents/{doc.public_id}/access-logs/"),
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 403


@pytest.mark.django_db
def test_api_security_events_requires_perm(client, charlie, tenant_acme):
    client.force_login(charlie)
    resp = client.get(
        _url("security-events/"),
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 403
