"""Tests for DMS Phase 2 — REPOSITORY & FOLDER STRUCTURE.

Covers:
  * Model creation (Repository, Folder)
  * Service layer: create/update/delete repository
  * Service layer: create/rename/move/delete folder
  * Materialized path computation and consistency
  * Cycle detection in move_folder
  * HTTP API: repositories CRUD + folders CRUD + children + move
  * IAM permission gates (403 when required perm absent)
  * Cross-tenant isolation
"""

from __future__ import annotations

import pytest

from simorgh.apps.dms.common.exceptions import AssetNotFound
from simorgh.apps.dms.repositories import queries, services
from simorgh.apps.dms.repositories.models import Folder, Repository
from simorgh.apps.dms.repositories.permissions import (
    PERM_FOLDER_MANAGE,
    PERM_FOLDER_VIEW,
    PERM_REPOSITORY_MANAGE,
    PERM_REPOSITORY_VIEW,
)
from simorgh.apps.dms.repositories.services import FolderError, RepositoryError
from simorgh.apps.iam.registry import sync_registry_to_db
from simorgh.core.context import RequestContext, use_request_context


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

ALL_REPO_PERMS = frozenset(
    {
        PERM_REPOSITORY_VIEW,
        PERM_REPOSITORY_MANAGE,
        PERM_FOLDER_VIEW,
        PERM_FOLDER_MANAGE,
    }
)


@pytest.fixture
def repo_ctx(alice, tenant_acme, acme_tree):
    """RequestContext with all DMS repo permissions."""
    return RequestContext(
        actor=alice,
        tenant=tenant_acme,
        org_node_ids=frozenset({acme_tree["root"].pk}),
        permissions=ALL_REPO_PERMS,
    )


@pytest.fixture
def repo_ctx_view_only(alice, tenant_acme, acme_tree):
    """RequestContext with only view permissions."""
    return RequestContext(
        actor=alice,
        tenant=tenant_acme,
        org_node_ids=frozenset({acme_tree["root"].pk}),
        permissions=frozenset({PERM_REPOSITORY_VIEW, PERM_FOLDER_VIEW}),
    )


@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):
    """A simple test repository (no service layer — direct ORM for speed)."""
    return Repository.objects.create(
        name="Main Repo",
        slug="main-repo",
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
    )


@pytest.fixture
def other_repo(tenant_acme, acme_tree):
    return Repository.objects.create(
        name="Other Repo",
        slug="other-repo",
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
    )


@pytest.fixture
def globex_repo(tenant_globex):
    from simorgh.apps.organizations.services import create_node

    root = create_node(tenant_id=tenant_globex.pk, name="HQ")
    return Repository.objects.create(
        name="Globex Main",
        slug="globex-main",
        tenant=tenant_globex,
        organization_node=root,
    )


# ---------------------------------------------------------------------------
# Service layer — Repository
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_create_repository(tenant_acme, acme_tree):
    repo = services.create_repository(
        name="HR Documents",
        slug="hr-docs",
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
        description="All HR files",
    )
    assert repo.pk is not None
    assert repo.name == "HR Documents"
    assert repo.slug == "hr-docs"
    assert repo.description == "All HR files"
    assert repo.tenant_id == tenant_acme.pk
    assert repo.is_deleted is False


@pytest.mark.django_db
def test_create_repository_duplicate_slug_raises(tenant_acme, acme_tree, repo):
    with pytest.raises(RepositoryError, match="already exists"):
        services.create_repository(
            name="Another",
            slug="main-repo",
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
        )


@pytest.mark.django_db
def test_create_repository_same_slug_different_tenant_ok(tenant_acme, acme_tree, repo, tenant_globex):
    from simorgh.apps.organizations.services import create_node

    globex_root = create_node(tenant_id=tenant_globex.pk, name="HQ")
    other = services.create_repository(
        name="Main Repo",
        slug="main-repo",  # same slug as repo but different tenant
        tenant_id=tenant_globex.pk,
        organization_node_id=globex_root.pk,
    )
    assert other.pk != repo.pk


@pytest.mark.django_db
def test_update_repository(repo):
    updated = services.update_repository(repo, name="Updated Name", description="New desc")
    updated.refresh_from_db()
    assert updated.name == "Updated Name"
    assert updated.description == "New desc"


@pytest.mark.django_db
def test_delete_repository_soft(repo):
    services.delete_repository(repo)
    assert not Repository.objects.filter(pk=repo.pk, is_deleted=False).exists()
    assert Repository.objects.with_deleted().filter(pk=repo.pk).exists()  # still in DB


@pytest.mark.django_db
def test_delete_repository_also_soft_deletes_folders(repo, tenant_acme, acme_tree):
    folder = services.create_folder(
        repository=repo,
        name="Contracts",
        parent=None,
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
    )
    services.delete_repository(repo)
    folder.refresh_from_db()
    assert folder.is_deleted is True


# ---------------------------------------------------------------------------
# Service layer — Folder
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_create_root_folder(repo, tenant_acme, acme_tree):
    folder = services.create_folder(
        repository=repo,
        name="Contracts",
        parent=None,
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
    )
    assert folder.pk is not None
    assert folder.name == "Contracts"
    assert folder.parent is None
    assert folder.depth == 0
    assert str(folder.public_id) in folder.materialized_path
    assert "/" not in folder.materialized_path  # root has no slash


@pytest.mark.django_db
def test_create_child_folder(repo, tenant_acme, acme_tree):
    parent = services.create_folder(
        repository=repo,
        name="Contracts",
        parent=None,
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
    )
    child = services.create_folder(
        repository=repo,
        name="2024",
        parent=parent,
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
    )
    assert child.depth == 1
    assert child.materialized_path == f"{parent.materialized_path}/{child.public_id}"


@pytest.mark.django_db
def test_create_grandchild_folder(repo, tenant_acme, acme_tree):
    root_f = services.create_folder(
        repository=repo, name="A", parent=None,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    child_f = services.create_folder(
        repository=repo, name="B", parent=root_f,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    grandchild = services.create_folder(
        repository=repo, name="C", parent=child_f,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    assert grandchild.depth == 2
    assert grandchild.materialized_path == (
        f"{root_f.public_id}/{child_f.public_id}/{grandchild.public_id}"
    )


@pytest.mark.django_db
def test_duplicate_name_same_parent_raises(repo, tenant_acme, acme_tree):
    services.create_folder(
        repository=repo, name="Finance", parent=None,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    with pytest.raises(FolderError, match="already exists"):
        services.create_folder(
            repository=repo, name="Finance", parent=None,
            tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
        )


@pytest.mark.django_db
def test_rename_folder(repo, tenant_acme, acme_tree):
    folder = services.create_folder(
        repository=repo, name="Old Name", parent=None,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    old_path = folder.materialized_path
    services.rename_folder(folder, new_name="New Name")
    folder.refresh_from_db()
    assert folder.name == "New Name"
    assert folder.materialized_path == old_path  # path unchanged — UUID-based


@pytest.mark.django_db
def test_rename_folder_duplicate_name_raises(repo, tenant_acme, acme_tree):
    services.create_folder(
        repository=repo, name="Alpha", parent=None,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    beta = services.create_folder(
        repository=repo, name="Beta", parent=None,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    with pytest.raises(FolderError, match="already exists"):
        services.rename_folder(beta, new_name="Alpha")


@pytest.mark.django_db
def test_move_folder_updates_path_and_depth(repo, tenant_acme, acme_tree):
    a = services.create_folder(
        repository=repo, name="A", parent=None,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    b = services.create_folder(
        repository=repo, name="B", parent=None,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    child_b = services.create_folder(
        repository=repo, name="C", parent=b,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    # Move A under B/C
    services.move_folder(a, new_parent=child_b)
    a.refresh_from_db()
    assert a.depth == 2
    assert a.materialized_path == f"{b.public_id}/{child_b.public_id}/{a.public_id}"


@pytest.mark.django_db
def test_move_folder_updates_descendants(repo, tenant_acme, acme_tree):
    root_a = services.create_folder(
        repository=repo, name="A", parent=None,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    child_a = services.create_folder(
        repository=repo, name="A1", parent=root_a,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    root_b = services.create_folder(
        repository=repo, name="B", parent=None,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    # Move A (and its child A1) under B
    services.move_folder(root_a, new_parent=root_b)
    child_a.refresh_from_db()
    assert child_a.depth == 2
    assert child_a.materialized_path == (
        f"{root_b.public_id}/{root_a.public_id}/{child_a.public_id}"
    )


@pytest.mark.django_db
def test_move_folder_cycle_raises(repo, tenant_acme, acme_tree):
    parent = services.create_folder(
        repository=repo, name="P", parent=None,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    child = services.create_folder(
        repository=repo, name="C", parent=parent,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    with pytest.raises(FolderError, match="descendant"):
        services.move_folder(parent, new_parent=child)


@pytest.mark.django_db
def test_move_folder_into_itself_raises(repo, tenant_acme, acme_tree):
    folder = services.create_folder(
        repository=repo, name="X", parent=None,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    with pytest.raises(FolderError, match="itself"):
        services.move_folder(folder, new_parent=folder)


@pytest.mark.django_db
def test_delete_folder_and_descendants(repo, tenant_acme, acme_tree):
    root_f = services.create_folder(
        repository=repo, name="Root", parent=None,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    child = services.create_folder(
        repository=repo, name="Child", parent=root_f,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    services.delete_folder(root_f)
    root_f.refresh_from_db()
    child.refresh_from_db()
    assert root_f.is_deleted is True
    assert child.is_deleted is True


# ---------------------------------------------------------------------------
# Query layer
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_get_repository_not_found(tenant_acme):
    import uuid

    with pytest.raises(AssetNotFound):
        queries.get_repository(tenant_acme.pk, str(uuid.uuid4()))


@pytest.mark.django_db
def test_list_repositories_tenant_isolation(repo, globex_repo, tenant_acme):
    result = queries.list_repositories(tenant_id=tenant_acme.pk)
    pks = list(result.values_list("pk", flat=True))
    assert repo.pk in pks
    assert globex_repo.pk not in pks


@pytest.mark.django_db
def test_list_descendants(repo, tenant_acme, acme_tree):
    root_f = services.create_folder(
        repository=repo, name="Root", parent=None,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    child = services.create_folder(
        repository=repo, name="Child", parent=root_f,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    grandchild = services.create_folder(
        repository=repo, name="Grand", parent=child,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    desc = list(queries.list_descendants(root_f))
    assert child in desc
    assert grandchild in desc
    assert root_f not in desc


# ---------------------------------------------------------------------------
# HTTP API — Repositories
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_api_list_repositories(client, alice, tenant_acme, acme_tree, alice_membership, repo, repo_ctx):
    client.force_login(alice)
    with use_request_context(repo_ctx):
        resp = client.get("/api/v1/dms/repositories/", HTTP_X_TENANT=tenant_acme.slug)
    assert resp.status_code == 200
    data = resp.json()
    assert isinstance(data, list)
    assert any(r["slug"] == "main-repo" for r in data)


@pytest.mark.django_db
def test_api_create_repository(client, alice, tenant_acme, acme_tree, alice_membership, repo_ctx):
    client.force_login(alice)
    with use_request_context(repo_ctx):
        resp = client.post(
            "/api/v1/dms/repositories/",
            data={"name": "Legal", "slug": "legal"},
            content_type="application/json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 201
    assert resp.json()["slug"] == "legal"


@pytest.mark.django_db
def test_api_create_repository_requires_perm(client, alice, tenant_acme, repo_ctx_view_only):
    client.force_login(alice)
    with use_request_context(repo_ctx_view_only):
        resp = client.post(
            "/api/v1/dms/repositories/",
            data={"name": "Nope", "slug": "nope"},
            content_type="application/json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 403


@pytest.mark.django_db
def test_api_get_repository(client, alice, tenant_acme, acme_tree, alice_membership, repo, repo_ctx):
    client.force_login(alice)
    with use_request_context(repo_ctx):
        resp = client.get(
            f"/api/v1/dms/repositories/{repo.public_id}/",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 200
    assert resp.json()["slug"] == "main-repo"


@pytest.mark.django_db
def test_api_update_repository(client, alice, tenant_acme, acme_tree, alice_membership, repo, repo_ctx):
    client.force_login(alice)
    with use_request_context(repo_ctx):
        resp = client.patch(
            f"/api/v1/dms/repositories/{repo.public_id}/",
            data={"name": "Main Repo Updated"},
            content_type="application/json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 200
    assert resp.json()["name"] == "Main Repo Updated"


@pytest.mark.django_db
def test_api_delete_repository(client, alice, tenant_acme, acme_tree, alice_membership, repo, repo_ctx):
    client.force_login(alice)
    with use_request_context(repo_ctx):
        resp = client.delete(
            f"/api/v1/dms/repositories/{repo.public_id}/",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 204
    assert not Repository.objects.filter(pk=repo.pk, is_deleted=False).exists()


# ---------------------------------------------------------------------------
# HTTP API — Folders
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_api_create_root_folder(client, alice, tenant_acme, acme_tree, alice_membership, repo, repo_ctx):
    client.force_login(alice)
    with use_request_context(repo_ctx):
        resp = client.post(
            f"/api/v1/dms/repositories/{repo.public_id}/folders/",
            data={"name": "Legal"},
            content_type="application/json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 201
    data = resp.json()
    assert data["name"] == "Legal"
    assert data["depth"] == 0


@pytest.mark.django_db
def test_api_get_folder(client, alice, tenant_acme, acme_tree, alice_membership, repo, repo_ctx):
    folder = services.create_folder(
        repository=repo, name="Finance", parent=None,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    client.force_login(alice)
    with use_request_context(repo_ctx):
        resp = client.get(
            f"/api/v1/dms/repositories/{repo.public_id}/folders/{folder.public_id}/",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 200
    assert resp.json()["name"] == "Finance"


@pytest.mark.django_db
def test_api_rename_folder(client, alice, tenant_acme, acme_tree, alice_membership, repo, repo_ctx):
    folder = services.create_folder(
        repository=repo, name="Old", parent=None,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    client.force_login(alice)
    with use_request_context(repo_ctx):
        resp = client.patch(
            f"/api/v1/dms/repositories/{repo.public_id}/folders/{folder.public_id}/",
            data={"name": "New Name"},
            content_type="application/json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 200
    assert resp.json()["name"] == "New Name"


@pytest.mark.django_db
def test_api_delete_folder(client, alice, tenant_acme, acme_tree, alice_membership, repo, repo_ctx):
    folder = services.create_folder(
        repository=repo, name="ToDelete", parent=None,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    client.force_login(alice)
    with use_request_context(repo_ctx):
        resp = client.delete(
            f"/api/v1/dms/repositories/{repo.public_id}/folders/{folder.public_id}/",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 204
    folder.refresh_from_db()
    assert folder.is_deleted is True


@pytest.mark.django_db
def test_api_folder_children(client, alice, tenant_acme, acme_tree, alice_membership, repo, repo_ctx):
    parent = services.create_folder(
        repository=repo, name="Parent", parent=None,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    child = services.create_folder(
        repository=repo, name="Child", parent=parent,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    client.force_login(alice)
    with use_request_context(repo_ctx):
        resp = client.get(
            f"/api/v1/dms/repositories/{repo.public_id}/folders/{parent.public_id}/children/",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 200
    names = [f["name"] for f in resp.json()]
    assert "Child" in names


@pytest.mark.django_db
def test_api_move_folder(client, alice, tenant_acme, acme_tree, alice_membership, repo, repo_ctx):
    dest = services.create_folder(
        repository=repo, name="Destination", parent=None,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    moving = services.create_folder(
        repository=repo, name="Moving", parent=None,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    client.force_login(alice)
    with use_request_context(repo_ctx):
        resp = client.post(
            f"/api/v1/dms/repositories/{repo.public_id}/folders/{moving.public_id}/move/",
            data={"parent_id": str(dest.public_id)},
            content_type="application/json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 200
    data = resp.json()
    assert data["depth"] == 1
    assert str(dest.public_id) in data["materialized_path"]


@pytest.mark.django_db
def test_api_move_folder_to_root(client, alice, tenant_acme, acme_tree, alice_membership, repo, repo_ctx):
    parent = services.create_folder(
        repository=repo, name="Parent", parent=None,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    child = services.create_folder(
        repository=repo, name="Child", parent=parent,
        tenant_id=tenant_acme.pk, organization_node_id=acme_tree["root"].pk,
    )
    client.force_login(alice)
    with use_request_context(repo_ctx):
        resp = client.post(
            f"/api/v1/dms/repositories/{repo.public_id}/folders/{child.public_id}/move/",
            data={"parent_id": None},
            content_type="application/json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
    assert resp.status_code == 200
    assert resp.json()["depth"] == 0
