"""S3.2 — Task Management backend tests.

Covers:
- TaskTag model + service (create/update/delete)
- Task model + service (create/update/delete, change_status, assign)
- TaskComment add/update/delete
- TaskWatcher watch/unwatch
- TaskAttachment add/delete
- Task selectors
- Event dispatch on key operations
- Permission codes registered
- API: task list, create, detail, update, delete, change-status, assign
- API: comment CRUD, watch/unwatch, subtasks, my-tasks
- API: tag CRUD
- Uniqueness constraints (tag per tenant)
"""

from __future__ import annotations

import pytest

from simorgh.apps.iam.registry import sync_registry_to_db
from simorgh.apps.memberships.models import Membership
from simorgh.apps.projects.models import Project, ProjectPhase, ProjectStatus
from simorgh.apps.tasks.models import (
    Task,
    TaskComment,
    TaskPriority,
    TaskStatus,
    TaskTag,
    TaskWatcher,
)
from simorgh.apps.tasks import services, selectors


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def alice_admin_membership(alice, tenant_acme, acme_tree, role_admin) -> 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 project(tenant_acme, acme_tree, alice):
    return Project.objects.create(
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
        name="Alpha",
        code="ALPHA",
        status=ProjectStatus.ACTIVE,
        owner=alice,
    )


@pytest.fixture
def phase(project):
    return ProjectPhase.objects.create(project=project, name="Planning", order=1)


@pytest.fixture
def task_tag(tenant_acme):
    return TaskTag.objects.create(tenant=tenant_acme, name="Bug", color="#ff0000")


@pytest.fixture
def task(tenant_acme, acme_tree, alice, project):
    return Task.objects.create(
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
        title="Fix login bug",
        reporter=alice,
        project=project,
        status=TaskStatus.TODO,
        priority=TaskPriority.HIGH,
    )


@pytest.fixture
def comment(task, alice):
    return TaskComment.objects.create(task=task, author=alice, body="Initial comment")


# ---------------------------------------------------------------------------
# TaskTag services
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestTaskTagServices:
    def test_create_tag(self, tenant_acme):
        tag = services.create_task_tag(tenant_id=tenant_acme.pk, name="Feature", color="#00f")
        assert tag.pk is not None
        assert tag.name == "Feature"

    def test_update_tag(self, task_tag):
        services.update_task_tag(task_tag, name="Critical Bug", color="#f00")
        task_tag.refresh_from_db()
        assert task_tag.name == "Critical Bug"

    def test_delete_tag(self, task_tag):
        pk = task_tag.pk
        services.delete_task_tag(task_tag)
        assert not TaskTag.objects.filter(pk=pk).exists()

    def test_tag_uniqueness(self, tenant_acme):
        services.create_task_tag(tenant_id=tenant_acme.pk, name="Duplicate")
        from django.db import IntegrityError
        with pytest.raises(IntegrityError):
            services.create_task_tag(tenant_id=tenant_acme.pk, name="Duplicate")


# ---------------------------------------------------------------------------
# Task services
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestTaskServices:
    def test_create_task(self, tenant_acme, acme_tree, alice):
        t = services.create_task(
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
            title="Implement search",
            reporter_id=alice.pk,
        )
        assert t.pk is not None
        assert t.title == "Implement search"
        assert t.status == TaskStatus.TODO
        assert str(t.public_id) != ""

    def test_create_task_with_project(self, tenant_acme, acme_tree, alice, project, phase):
        t = services.create_task(
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
            title="Phase Task",
            reporter_id=alice.pk,
            project_id=project.pk,
            phase_id=phase.pk,
            assignee_id=alice.pk,
            priority="high",
        )
        assert t.project_id == project.pk
        assert t.phase_id == phase.pk
        assert t.assignee_id == alice.pk
        assert t.priority == "high"

    def test_update_task(self, task):
        services.update_task(task, title="Updated Title", priority="urgent")
        task.refresh_from_db()
        assert task.title == "Updated Title"
        assert task.priority == "urgent"

    def test_delete_task(self, task):
        pk = task.pk
        services.delete_task(task)
        assert not Task.objects.filter(pk=pk).exists()

    def test_change_status(self, task):
        services.change_task_status(task, new_status=TaskStatus.IN_PROGRESS)
        task.refresh_from_db()
        assert task.status == TaskStatus.IN_PROGRESS
        assert task.status_changes.count() == 1

    def test_change_status_same_noop(self, task):
        services.change_task_status(task, new_status=TaskStatus.TODO)
        assert task.status_changes.count() == 0

    def test_assign_task(self, task, alice):
        services.assign_task(task, user_id=alice.pk)
        task.refresh_from_db()
        assert task.assignee_id == alice.pk

    def test_subtask_creation(self, tenant_acme, acme_tree, alice, task):
        sub = services.create_task(
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
            title="Subtask A",
            reporter_id=alice.pk,
            parent_id=task.pk,
        )
        assert sub.parent_id == task.pk
        assert task.subtasks.count() == 1


# ---------------------------------------------------------------------------
# Comment services
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestCommentServices:
    def test_add_comment(self, task, alice):
        c = services.add_comment(task, author_id=alice.pk, body="Looks good!")
        assert c.pk is not None
        assert c.body == "Looks good!"
        assert not c.is_edited

    def test_add_empty_comment_raises(self, task, alice):
        with pytest.raises(ValueError, match="empty"):
            services.add_comment(task, author_id=alice.pk, body="  ")

    def test_update_comment(self, comment):
        services.update_comment(comment, body="Updated body")
        comment.refresh_from_db()
        assert comment.body == "Updated body"
        assert comment.is_edited

    def test_delete_comment(self, comment):
        pk = comment.pk
        services.delete_comment(comment)
        assert not TaskComment.objects.filter(pk=pk).exists()


# ---------------------------------------------------------------------------
# Watcher services
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestWatcherServices:
    def test_watch_task(self, task, alice):
        w = services.watch_task(task, user_id=alice.pk)
        assert w.pk is not None
        assert selectors.is_watching(task.pk, alice.pk)

    def test_watch_idempotent(self, task, alice):
        services.watch_task(task, user_id=alice.pk)
        services.watch_task(task, user_id=alice.pk)
        assert TaskWatcher.objects.filter(task=task, user=alice).count() == 1

    def test_unwatch_task(self, task, alice):
        services.watch_task(task, user_id=alice.pk)
        services.unwatch_task(task, user_id=alice.pk)
        assert not selectors.is_watching(task.pk, alice.pk)


# ---------------------------------------------------------------------------
# Selectors
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestTaskSelectors:
    def test_tasks_for_tenant(self, task, tenant_acme):
        qs = selectors.tasks_for_tenant(tenant_acme.pk)
        assert task in qs

    def test_tasks_for_project(self, task, tenant_acme, project):
        qs = selectors.tasks_for_project(tenant_acme.pk, project.pk)
        assert task in qs

    def test_tasks_my(self, tenant_acme, acme_tree, alice, task):
        task.assignee = alice
        task.save()
        qs = selectors.tasks_my(tenant_acme.pk, alice.pk)
        assert task in qs

    def test_subtasks_for_task(self, tenant_acme, acme_tree, alice, task):
        sub = services.create_task(
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
            title="Sub",
            reporter_id=alice.pk,
            parent_id=task.pk,
        )
        qs = selectors.subtasks_for_task(task.pk)
        assert sub in qs

    def test_task_by_public_id(self, task, tenant_acme):
        found = selectors.task_by_public_id(tenant_acme.pk, str(task.public_id))
        assert found == task


# ---------------------------------------------------------------------------
# Permissions
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_permissions_registered(db):
    sync_registry_to_db()
    from simorgh.apps.iam.models import Permission
    codes = set(Permission.objects.values_list("codename", flat=True))
    for perm in [
        "tasks.task.view", "tasks.task.create", "tasks.task.update", "tasks.task.delete",
        "tasks.task.manage_others", "tasks.comment.create", "tasks.comment.delete_own",
        "tasks.tag.manage",
    ]:
        assert perm in codes, f"Permission {perm!r} not registered"


# ---------------------------------------------------------------------------
# API tests
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestTasksAPI:
    # ── Tags ────────────────────────────────────────────────────────────────

    def test_list_tags(self, client, alice, alice_admin_membership, task_tag):
        client.force_login(alice)
        resp = client.get("/api/v1/tasks/tags/")
        assert resp.status_code == 200
        names = [t["name"] for t in resp.json()]
        assert "Bug" in names

    def test_create_tag_api(self, client, alice, alice_admin_membership):
        client.force_login(alice)
        resp = client.post(
            "/api/v1/tasks/tags/",
            data={"name": "Feature", "color": "#0f0"},
            content_type="application/json",
        )
        assert resp.status_code == 201
        assert resp.json()["name"] == "Feature"

    def test_update_tag_api(self, client, alice, alice_admin_membership, task_tag):
        client.force_login(alice)
        resp = client.patch(
            f"/api/v1/tasks/tags/{task_tag.pk}/",
            data={"name": "Critical"},
            content_type="application/json",
        )
        assert resp.status_code == 200
        assert resp.json()["name"] == "Critical"

    def test_delete_tag_api(self, client, alice, alice_admin_membership, task_tag):
        client.force_login(alice)
        resp = client.delete(f"/api/v1/tasks/tags/{task_tag.pk}/")
        assert resp.status_code == 204
        assert not TaskTag.objects.filter(pk=task_tag.pk).exists()

    # ── Tasks ────────────────────────────────────────────────────────────────

    def test_list_tasks(self, client, alice, alice_admin_membership, task):
        client.force_login(alice)
        resp = client.get("/api/v1/tasks/")
        assert resp.status_code == 200
        assert any(t["title"] == "Fix login bug" for t in resp.json())

    def test_create_task_api(self, client, alice, alice_admin_membership, project):
        client.force_login(alice)
        resp = client.post(
            "/api/v1/tasks/",
            data={"title": "New Task", "project_id": project.pk, "priority": "medium"},
            content_type="application/json",
        )
        assert resp.status_code == 201
        assert resp.json()["title"] == "New Task"

    def test_get_task_detail(self, client, alice, alice_admin_membership, task):
        client.force_login(alice)
        resp = client.get(f"/api/v1/tasks/{task.public_id}/")
        assert resp.status_code == 200
        data = resp.json()
        assert data["title"] == "Fix login bug"
        assert "description" in data
        assert "status_history" in data

    def test_update_task_api(self, client, alice, alice_admin_membership, task):
        client.force_login(alice)
        resp = client.patch(
            f"/api/v1/tasks/{task.public_id}/",
            data={"title": "Updated Bug Fix", "priority": "urgent"},
            content_type="application/json",
        )
        assert resp.status_code == 200
        assert resp.json()["title"] == "Updated Bug Fix"
        assert resp.json()["priority"] == "urgent"

    def test_delete_task_api(self, client, alice, alice_admin_membership, task):
        client.force_login(alice)
        resp = client.delete(f"/api/v1/tasks/{task.public_id}/")
        assert resp.status_code == 204
        assert not Task.objects.filter(pk=task.pk).exists()

    def test_change_status_api(self, client, alice, alice_admin_membership, task):
        client.force_login(alice)
        resp = client.post(
            f"/api/v1/tasks/{task.public_id}/status/",
            data={"status": "in_progress"},
            content_type="application/json",
        )
        assert resp.status_code == 200
        task.refresh_from_db()
        assert task.status == "in_progress"

    def test_assign_api(self, client, alice, alice_admin_membership, task):
        client.force_login(alice)
        resp = client.post(
            f"/api/v1/tasks/{task.public_id}/assign/",
            data={"user_id": alice.pk},
            content_type="application/json",
        )
        assert resp.status_code == 200
        task.refresh_from_db()
        assert task.assignee_id == alice.pk

    def test_filter_by_status(self, client, alice, alice_admin_membership, task):
        client.force_login(alice)
        resp = client.get("/api/v1/tasks/?status=todo")
        assert resp.status_code == 200
        statuses = {t["status"] for t in resp.json()}
        assert statuses == {"todo"} or "todo" in statuses

    def test_filter_by_project(self, client, alice, alice_admin_membership, task, project):
        client.force_login(alice)
        resp = client.get(f"/api/v1/tasks/?project={project.pk}")
        assert resp.status_code == 200
        assert any(t["title"] == "Fix login bug" for t in resp.json())

    # ── My Tasks ────────────────────────────────────────────────────────────

    def test_my_tasks(self, client, alice, alice_admin_membership, task):
        task.assignee = alice
        task.save()
        client.force_login(alice)
        resp = client.get("/api/v1/tasks/my/")
        assert resp.status_code == 200
        assert any(t["title"] == "Fix login bug" for t in resp.json())

    # ── Subtasks ─────────────────────────────────────────────────────────────

    def test_subtasks_api(self, client, alice, alice_admin_membership, task, tenant_acme, acme_tree):
        sub = services.create_task(
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
            title="Subtask",
            reporter_id=alice.pk,
            parent_id=task.pk,
        )
        client.force_login(alice)
        resp = client.get(f"/api/v1/tasks/{task.public_id}/subtasks/")
        assert resp.status_code == 200
        assert any(t["title"] == "Subtask" for t in resp.json())

    # ── Comments ─────────────────────────────────────────────────────────────

    def test_list_comments(self, client, alice, alice_admin_membership, task, comment):
        client.force_login(alice)
        resp = client.get(f"/api/v1/tasks/{task.public_id}/comments/")
        assert resp.status_code == 200
        assert any(c["body"] == "Initial comment" for c in resp.json())

    def test_add_comment_api(self, client, alice, alice_admin_membership, task):
        client.force_login(alice)
        resp = client.post(
            f"/api/v1/tasks/{task.public_id}/comments/",
            data={"body": "Great work!"},
            content_type="application/json",
        )
        assert resp.status_code == 201
        assert resp.json()["body"] == "Great work!"

    def test_add_empty_comment_rejected(self, client, alice, alice_admin_membership, task):
        client.force_login(alice)
        resp = client.post(
            f"/api/v1/tasks/{task.public_id}/comments/",
            data={"body": ""},
            content_type="application/json",
        )
        assert resp.status_code == 400

    def test_edit_comment_api(self, client, alice, alice_admin_membership, task, comment):
        client.force_login(alice)
        resp = client.patch(
            f"/api/v1/tasks/{task.public_id}/comments/{comment.pk}/",
            data={"body": "Edited comment"},
            content_type="application/json",
        )
        assert resp.status_code == 200
        assert resp.json()["is_edited"]

    def test_delete_comment_api(self, client, alice, alice_admin_membership, task, comment):
        client.force_login(alice)
        resp = client.delete(f"/api/v1/tasks/{task.public_id}/comments/{comment.pk}/")
        assert resp.status_code == 204
        assert not TaskComment.objects.filter(pk=comment.pk).exists()

    # ── Watch ────────────────────────────────────────────────────────────────

    def test_watch_api(self, client, alice, alice_admin_membership, task):
        client.force_login(alice)
        resp = client.post(
            f"/api/v1/tasks/{task.public_id}/watch/",
            content_type="application/json",
        )
        assert resp.status_code == 200
        assert resp.json()["watching"] is True
        assert TaskWatcher.objects.filter(task=task, user=alice).exists()

    def test_unwatch_api(self, client, alice, alice_admin_membership, task):
        services.watch_task(task, user_id=alice.pk)
        client.force_login(alice)
        resp = client.delete(f"/api/v1/tasks/{task.public_id}/watch/")
        assert resp.status_code == 200
        assert resp.json()["watching"] is False
        assert not TaskWatcher.objects.filter(task=task, user=alice).exists()

    # ── Auth guard ───────────────────────────────────────────────────────────

    def test_unauthenticated_access(self, client):
        resp = client.get("/api/v1/tasks/")
        assert resp.status_code == 401
