"""Tests for DMS Phase 1 — FILE FOUNDATION.

Covers:
  * Service-layer: upload_asset, initiate_session, receive_chunk, complete_session
  * Repository-layer: list_assets, get_asset, cross-tenant isolation
  * HTTP API: direct upload, list, retrieve, delete, variants, sessions
  * IAM: permission gate (403 when required perm absent)
"""

from __future__ import annotations

import io
import json

import pytest

from simorgh.apps.dms.assets import repositories, services
from simorgh.apps.dms.assets.constants import APP_CONTEXT
from simorgh.apps.dms.assets.permissions import (
    PERM_ASSET_DELETE,
    PERM_ASSET_UPLOAD,
    PERM_ASSET_VIEW,
)
from simorgh.apps.dms.common.exceptions import AssetNotFound, UploadSessionError
from simorgh.apps.iam.registry import sync_registry_to_db
from simorgh.apps.storage.models import FileMetadata, FileUploadStatus, UploadSessionStatus
from simorgh.core.context import RequestContext, use_request_context


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

ALL_DMS_PERMS = frozenset(
    {PERM_ASSET_VIEW, PERM_ASSET_UPLOAD, PERM_ASSET_DELETE, "dms.asset.download"}
)


@pytest.fixture
def dms_ctx(alice, tenant_acme, acme_tree):
    """A request context with all DMS permissions bound."""
    return RequestContext(
        actor=alice,
        tenant=tenant_acme,
        org_node_ids=frozenset({acme_tree["root"].pk}),
        permissions=ALL_DMS_PERMS,
    )


@pytest.fixture
def alice_admin_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


# ---------------------------------------------------------------------------
# Service layer — direct upload
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_upload_asset_creates_file_metadata(dms_ctx, tenant_acme, acme_tree):
    with use_request_context(dms_ctx):
        asset = services.upload_asset(
            filename="hello.txt",
            content=io.BytesIO(b"hello world"),
            content_type="text/plain",
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
        )

    assert asset.pk is not None
    assert asset.filename == "hello.txt"
    assert asset.content_type == "text/plain"
    assert asset.size_bytes == 11
    assert asset.app_context == APP_CONTEXT
    assert asset.tenant_id == tenant_acme.pk
    assert asset.upload_status == FileUploadStatus.READY


@pytest.mark.django_db
def test_upload_asset_is_tagged_dms(dms_ctx, tenant_acme, acme_tree):
    """FileMetadata created via DMS service must have app_context='dms'."""
    with use_request_context(dms_ctx):
        asset = services.upload_asset(
            filename="doc.pdf",
            content=io.BytesIO(b"%PDF"),
            content_type="application/pdf",
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
        )
    assert FileMetadata.objects.filter(pk=asset.pk, app_context="dms").exists()


# ---------------------------------------------------------------------------
# Service layer — upload session
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_initiate_session_creates_upload_session(dms_ctx, tenant_acme, acme_tree):
    with use_request_context(dms_ctx):
        session = services.initiate_session(
            filename="video.mp4",
            content_type="video/mp4",
            total_size_bytes=1024 * 1024,
            total_chunks=3,
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
        )

    assert session.pk is not None
    assert session.filename == "video.mp4"
    assert session.total_chunks == 3
    assert session.received_chunks == 0
    assert session.app_context == APP_CONTEXT
    assert session.tenant_id == tenant_acme.pk
    assert session.status == UploadSessionStatus.INITIATED


@pytest.mark.django_db
def test_complete_session_single_chunk(dms_ctx, tenant_acme, acme_tree):
    """Single-chunk session flow: initiate → receive_chunk → complete → FileMetadata."""
    content = b"The quick brown fox"

    with use_request_context(dms_ctx):
        session = services.initiate_session(
            filename="note.txt",
            content_type="text/plain",
            total_size_bytes=len(content),
            total_chunks=1,
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
        )
        session = services.receive_chunk(session, chunk_index=0, chunk_data=io.BytesIO(content))
        asset = services.complete_session(session)

    assert asset.filename == "note.txt"
    assert asset.size_bytes == len(content)
    assert asset.upload_status == FileUploadStatus.READY
    assert asset.app_context == APP_CONTEXT
    assert asset.tenant_id == tenant_acme.pk

    # Session must be COMPLETED
    session.refresh_from_db()
    assert session.status == UploadSessionStatus.COMPLETED
    assert session.completed_file_id == asset.pk


@pytest.mark.django_db
def test_complete_session_multi_chunk(dms_ctx, tenant_acme, acme_tree):
    """Three-chunk session assembles in correct order."""
    chunks = [b"chunk-0-", b"chunk-1-", b"chunk-2"]
    full = b"".join(chunks)

    with use_request_context(dms_ctx):
        session = services.initiate_session(
            filename="multi.bin",
            content_type="application/octet-stream",
            total_size_bytes=len(full),
            total_chunks=3,
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
        )
        for i, chunk in enumerate(chunks):
            session = services.receive_chunk(session, chunk_index=i, chunk_data=io.BytesIO(chunk))
        asset = services.complete_session(session)

    assert asset.size_bytes == len(full)


@pytest.mark.django_db
def test_complete_session_fails_if_chunks_missing(dms_ctx, tenant_acme, acme_tree):
    """complete_session raises UploadSessionError when not all chunks received."""
    with use_request_context(dms_ctx):
        session = services.initiate_session(
            filename="incomplete.bin",
            content_type="application/octet-stream",
            total_size_bytes=100,
            total_chunks=2,
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
        )
        # Only send 1 of 2 chunks
        services.receive_chunk(session, chunk_index=0, chunk_data=io.BytesIO(b"x" * 50))

        with pytest.raises(UploadSessionError, match="Cannot complete"):
            services.complete_session(session)


# ---------------------------------------------------------------------------
# Repository layer
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_list_assets_scoped_to_tenant(dms_ctx, tenant_acme, tenant_globex, acme_tree):
    """list_assets returns only DMS assets for the given tenant."""
    from simorgh.apps.organizations.services import create_node

    root_globex = create_node(tenant_id=tenant_globex.pk, name="HQ")
    globex_ctx = RequestContext(
        actor=dms_ctx.actor,
        tenant=tenant_globex,
        org_node_ids=frozenset({root_globex.pk}),
        permissions=ALL_DMS_PERMS,
    )

    with use_request_context(dms_ctx):
        services.upload_asset(
            filename="acme.txt",
            content=io.BytesIO(b"acme"),
            content_type="text/plain",
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
        )

    with use_request_context(globex_ctx):
        services.upload_asset(
            filename="globex.txt",
            content=io.BytesIO(b"globex"),
            content_type="text/plain",
            tenant_id=tenant_globex.pk,
            organization_node_id=root_globex.pk,
        )

    acme_qs = repositories.list_assets(tenant_id=tenant_acme.pk)
    assert acme_qs.count() == 1
    assert acme_qs.first().filename == "acme.txt"

    globex_qs = repositories.list_assets(tenant_id=tenant_globex.pk)
    assert globex_qs.count() == 1


@pytest.mark.django_db
def test_get_asset_raises_not_found_for_wrong_tenant(dms_ctx, tenant_globex, acme_tree):
    """get_asset raises AssetNotFound when the asset belongs to a different tenant."""
    with use_request_context(dms_ctx):
        asset = services.upload_asset(
            filename="private.txt",
            content=io.BytesIO(b"secret"),
            content_type="text/plain",
            tenant_id=dms_ctx.tenant.pk,
            organization_node_id=acme_tree["root"].pk,
        )

    with pytest.raises(AssetNotFound):
        repositories.get_asset(
            tenant_id=tenant_globex.pk,
            public_id=str(asset.public_id),
        )


@pytest.mark.django_db
def test_get_asset_raises_not_found_for_deleted(dms_ctx, acme_tree):
    """get_asset raises AssetNotFound for soft-deleted records."""
    with use_request_context(dms_ctx):
        asset = services.upload_asset(
            filename="todelete.txt",
            content=io.BytesIO(b"bye"),
            content_type="text/plain",
            tenant_id=dms_ctx.tenant.pk,
            organization_node_id=acme_tree["root"].pk,
        )

    with use_request_context(dms_ctx):
        asset.delete()

    with pytest.raises(AssetNotFound):
        repositories.get_asset(tenant_id=dms_ctx.tenant.pk, public_id=str(asset.public_id))


@pytest.mark.django_db
def test_list_variants_returns_empty_for_fresh_asset(dms_ctx, acme_tree):
    with use_request_context(dms_ctx):
        asset = services.upload_asset(
            filename="fresh.txt",
            content=io.BytesIO(b"hi"),
            content_type="text/plain",
            tenant_id=dms_ctx.tenant.pk,
            organization_node_id=acme_tree["root"].pk,
        )

    variants = repositories.list_variants(asset)
    assert variants.count() == 0


@pytest.mark.django_db
def test_list_assets_filter_by_upload_status(dms_ctx, acme_tree):
    with use_request_context(dms_ctx):
        services.upload_asset(
            filename="ready.txt",
            content=io.BytesIO(b"ready"),
            content_type="text/plain",
            tenant_id=dms_ctx.tenant.pk,
            organization_node_id=acme_tree["root"].pk,
        )

    qs_ready = repositories.list_assets(
        tenant_id=dms_ctx.tenant.pk, upload_status=FileUploadStatus.READY
    )
    qs_pending = repositories.list_assets(
        tenant_id=dms_ctx.tenant.pk, upload_status=FileUploadStatus.PENDING
    )
    assert qs_ready.count() == 1
    assert qs_pending.count() == 0


@pytest.mark.django_db
def test_list_assets_filter_by_search(dms_ctx, acme_tree):
    with use_request_context(dms_ctx):
        services.upload_asset(
            filename="annual_report_2024.pdf",
            content=io.BytesIO(b"pdf"),
            content_type="application/pdf",
            tenant_id=dms_ctx.tenant.pk,
            organization_node_id=acme_tree["root"].pk,
        )
        services.upload_asset(
            filename="invoice.xlsx",
            content=io.BytesIO(b"xlsx"),
            content_type="application/vnd.openxmlformats",
            tenant_id=dms_ctx.tenant.pk,
            organization_node_id=acme_tree["root"].pk,
        )

    qs = repositories.list_assets(tenant_id=dms_ctx.tenant.pk, search="annual")
    assert qs.count() == 1
    assert qs.first().filename == "annual_report_2024.pdf"


# ---------------------------------------------------------------------------
# HTTP API — direct upload
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_api_direct_upload_creates_asset(
    api_client, alice, alice_admin_membership, tenant_acme,
):
    sync_registry_to_db()
    api_client.force_login(alice)

    resp = api_client.post(
        "/api/v1/dms/assets/",
        data={"file": io.BytesIO(b"hello api")},
        format="multipart",
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 201, resp.content
    body = resp.json()
    assert body["filename"] is not None
    assert body["upload_status"] == FileUploadStatus.READY
    assert body["app_context"] == APP_CONTEXT


@pytest.mark.django_db
def test_api_list_assets(
    api_client, alice, alice_admin_membership, tenant_acme, acme_tree,
):
    sync_registry_to_db()
    api_client.force_login(alice)

    # Upload one asset first
    ctx = RequestContext(
        actor=alice,
        tenant=tenant_acme,
        org_node_ids=frozenset({acme_tree["root"].pk}),
        permissions=ALL_DMS_PERMS,
    )
    with use_request_context(ctx):
        services.upload_asset(
            filename="listed.txt",
            content=io.BytesIO(b"list me"),
            content_type="text/plain",
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
        )

    resp = api_client.get("/api/v1/dms/assets/", HTTP_X_TENANT=tenant_acme.slug)
    assert resp.status_code == 200
    filenames = [a["filename"] for a in resp.json()]
    assert "listed.txt" in filenames


@pytest.mark.django_db
def test_api_retrieve_asset(
    api_client, alice, alice_admin_membership, tenant_acme, acme_tree,
):
    sync_registry_to_db()
    api_client.force_login(alice)

    ctx = RequestContext(
        actor=alice,
        tenant=tenant_acme,
        org_node_ids=frozenset({acme_tree["root"].pk}),
        permissions=ALL_DMS_PERMS,
    )
    with use_request_context(ctx):
        asset = services.upload_asset(
            filename="retrieve_me.txt",
            content=io.BytesIO(b"hi"),
            content_type="text/plain",
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
        )

    resp = api_client.get(
        f"/api/v1/dms/assets/{asset.public_id}/",
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 200
    assert resp.json()["filename"] == "retrieve_me.txt"


@pytest.mark.django_db
def test_api_delete_asset(
    api_client, alice, alice_admin_membership, tenant_acme, acme_tree,
):
    sync_registry_to_db()
    api_client.force_login(alice)

    ctx = RequestContext(
        actor=alice,
        tenant=tenant_acme,
        org_node_ids=frozenset({acme_tree["root"].pk}),
        permissions=ALL_DMS_PERMS,
    )
    with use_request_context(ctx):
        asset = services.upload_asset(
            filename="delete_me.txt",
            content=io.BytesIO(b"bye"),
            content_type="text/plain",
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
        )

    resp = api_client.delete(
        f"/api/v1/dms/assets/{asset.public_id}/",
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 204

    asset.refresh_from_db()
    assert asset.is_deleted is True


@pytest.mark.django_db
def test_api_list_variants_empty(
    api_client, alice, alice_admin_membership, tenant_acme, acme_tree,
):
    sync_registry_to_db()
    api_client.force_login(alice)

    ctx = RequestContext(
        actor=alice,
        tenant=tenant_acme,
        org_node_ids=frozenset({acme_tree["root"].pk}),
        permissions=ALL_DMS_PERMS,
    )
    with use_request_context(ctx):
        asset = services.upload_asset(
            filename="novariants.txt",
            content=io.BytesIO(b"x"),
            content_type="text/plain",
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
        )

    resp = api_client.get(
        f"/api/v1/dms/assets/{asset.public_id}/variants/",
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 200
    assert resp.json() == []


# ---------------------------------------------------------------------------
# HTTP API — upload sessions
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_api_create_session(
    api_client, alice, alice_admin_membership, tenant_acme,
):
    sync_registry_to_db()
    api_client.force_login(alice)

    resp = api_client.post(
        "/api/v1/dms/assets/sessions/",
        data=json.dumps({
            "filename": "bigfile.bin",
            "content_type": "application/octet-stream",
            "total_size_bytes": 1024 * 1024,
            "total_chunks": 3,
        }),
        content_type="application/json",
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 201, resp.content
    body = resp.json()
    assert body["filename"] == "bigfile.bin"
    assert body["total_chunks"] == 3
    assert body["received_chunks"] == 0
    assert "public_id" in body


@pytest.mark.django_db
def test_api_session_chunk_and_complete(
    api_client, alice, alice_admin_membership, tenant_acme,
):
    sync_registry_to_db()
    api_client.force_login(alice)

    # 1. Create session
    resp = api_client.post(
        "/api/v1/dms/assets/sessions/",
        data=json.dumps({
            "filename": "chunked.txt",
            "content_type": "text/plain",
            "total_size_bytes": 11,
            "total_chunks": 1,
        }),
        content_type="application/json",
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 201
    session_id = resp.json()["public_id"]

    # 2. Upload chunk
    resp = api_client.post(
        f"/api/v1/dms/assets/sessions/{session_id}/chunk/",
        data={"chunk_index": 0, "data": io.BytesIO(b"hello world")},
        format="multipart",
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 200, resp.content
    assert resp.json()["received_chunks"] == 1

    # 3. Complete session
    resp = api_client.post(
        f"/api/v1/dms/assets/sessions/{session_id}/complete/",
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 201, resp.content
    body = resp.json()
    assert body["filename"] == "chunked.txt"
    assert body["upload_status"] == FileUploadStatus.READY
    assert body["app_context"] == APP_CONTEXT


@pytest.mark.django_db
def test_api_session_status(
    api_client, alice, alice_admin_membership, tenant_acme,
):
    sync_registry_to_db()
    api_client.force_login(alice)

    create_resp = api_client.post(
        "/api/v1/dms/assets/sessions/",
        data=json.dumps({
            "filename": "status_test.bin",
            "content_type": "application/octet-stream",
            "total_size_bytes": 512,
            "total_chunks": 1,
        }),
        content_type="application/json",
        HTTP_X_TENANT=tenant_acme.slug,
    )
    session_id = create_resp.json()["public_id"]

    resp = api_client.get(
        f"/api/v1/dms/assets/sessions/{session_id}/",
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 200
    assert resp.json()["status"] == UploadSessionStatus.INITIATED


# ---------------------------------------------------------------------------
# HTTP API — permission gates
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_api_upload_requires_perm(
    api_client, alice, alice_membership, tenant_acme,
):
    """alice_membership has viewer role (no dms perms) → 403."""
    sync_registry_to_db()
    api_client.force_login(alice)

    resp = api_client.post(
        "/api/v1/dms/assets/",
        data={"file": io.BytesIO(b"blocked")},
        format="multipart",
        HTTP_X_TENANT=tenant_acme.slug,
    )
    assert resp.status_code == 403


@pytest.mark.django_db
def test_api_list_requires_perm(
    api_client, alice, alice_membership, tenant_acme,
):
    """alice_membership has viewer role (no dms perms) → 403 on list."""
    sync_registry_to_db()
    api_client.force_login(alice)

    resp = api_client.get("/api/v1/dms/assets/", HTTP_X_TENANT=tenant_acme.slug)
    assert resp.status_code == 403


@pytest.mark.django_db
def test_api_unauthenticated_is_denied(api_client, tenant_acme):
    resp = api_client.get("/api/v1/dms/assets/", HTTP_X_TENANT=tenant_acme.slug)
    assert resp.status_code == 401
