"""S3.1 — Project Management backend tests.

Covers:
- ProjectTag model + service (create/update/delete)
- Project model + service (create/update/delete, change_status)
- ProjectMember add/remove
- ProjectPhase CRUD
- Milestone CRUD + complete
- Project selectors
- Event dispatch on key operations
- Permission codes registered
- API: project list, create, detail, update, delete, change-status
- API: member add/remove, phase CRUD, milestone CRUD + complete, tag CRUD
- Uniqueness constraints (code per tenant)
"""

from __future__ import annotations

import pytest
from django.urls import reverse

from simorgh.apps.crm.models import ContactTag  # noqa: F401 – just to verify no import clashes
from simorgh.apps.iam.registry import sync_registry_to_db
from simorgh.apps.memberships.models import Membership
from simorgh.apps.projects.models import (
    Milestone,
    Project,
    ProjectMember,
    ProjectPhase,
    ProjectStatus,
    ProjectTag,
)
from simorgh.apps.projects 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 proj_tag(tenant_acme) -> ProjectTag:
    return ProjectTag.objects.create(tenant=tenant_acme, name="Backend", color="#007bff")


@pytest.fixture
def project(tenant_acme, acme_tree, alice) -> Project:
    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) -> ProjectPhase:
    return ProjectPhase.objects.create(
        project=project,
        name="Planning",
        order=1,
    )


@pytest.fixture
def milestone(project) -> Milestone:
    return Milestone.objects.create(
        project=project,
        name="MVP Released",
        due_date="2025-12-31",
    )


# ---------------------------------------------------------------------------
# ProjectTag services
# ---------------------------------------------------------------------------


@pytest.mark.django_db
class TestProjectTagServices:
    def test_create_tag(self, tenant_acme):
        tag = services.create_project_tag(tenant_id=tenant_acme.pk, name="Urgent", color="#f00")
        assert tag.pk is not None
        assert tag.name == "Urgent"
        assert tag.tenant_id == tenant_acme.pk

    def test_update_tag(self, proj_tag):
        updated = services.update_project_tag(proj_tag, name="Frontend")
        assert updated.name == "Frontend"

    def test_delete_tag(self, proj_tag):
        tag_id = proj_tag.pk
        services.delete_project_tag(proj_tag)
        assert not ProjectTag.objects.filter(pk=tag_id).exists()

    def test_unique_tag_per_tenant(self, tenant_acme, proj_tag):
        from django.db import IntegrityError
        with pytest.raises(IntegrityError):
            ProjectTag.objects.create(tenant=tenant_acme, name="Backend", color="#000")


# ---------------------------------------------------------------------------
# Project model
# ---------------------------------------------------------------------------


@pytest.mark.django_db
class TestProjectModel:
    def test_create_project(self, tenant_acme, acme_tree, alice):
        proj = services.create_project(
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
            owner_id=alice.pk,
            name="Gamma",
            code="GAMMA",
        )
        assert proj.pk is not None
        assert proj.status == ProjectStatus.DRAFT

    def test_unique_code_per_tenant(self, project, tenant_acme, acme_tree, alice):
        from django.db import IntegrityError
        with pytest.raises(IntegrityError):
            Project.objects.create(
                tenant=tenant_acme,
                organization_node=acme_tree["root"],
                name="Alpha Duplicate",
                code="ALPHA",
                owner=alice,
            )

    def test_update_project(self, project):
        updated = services.update_project(project, name="Alpha Updated")
        assert updated.name == "Alpha Updated"

    def test_change_status(self, project):
        updated = services.change_project_status(project, new_status=ProjectStatus.COMPLETED)
        assert updated.status == ProjectStatus.COMPLETED

    def test_delete_project(self, project):
        pid = project.pk
        services.delete_project(project)
        assert not Project.objects.filter(pk=pid).exists()


# ---------------------------------------------------------------------------
# ProjectMember services
# ---------------------------------------------------------------------------


@pytest.mark.django_db
class TestProjectMemberServices:
    def test_add_member(self, project, alice):
        member = services.add_project_member(project, user_id=alice.pk, role="member")
        assert member.pk is not None
        assert member.role == "member"

    def test_remove_member(self, project, alice):
        services.add_project_member(project, user_id=alice.pk)
        services.remove_project_member(project, user_id=alice.pk)
        assert not ProjectMember.objects.filter(project=project, user=alice).exists()


# ---------------------------------------------------------------------------
# Phase services
# ---------------------------------------------------------------------------


@pytest.mark.django_db
class TestPhaseServices:
    def test_create_phase(self, project):
        ph = services.create_phase(project, name="Design", order=1)
        assert ph.pk is not None
        assert ph.name == "Design"

    def test_update_phase(self, phase):
        updated = services.update_phase(phase, name="Execution")
        assert updated.name == "Execution"

    def test_delete_phase(self, phase):
        ph_id = phase.pk
        services.delete_phase(phase)
        assert not ProjectPhase.objects.filter(pk=ph_id).exists()


# ---------------------------------------------------------------------------
# Milestone services
# ---------------------------------------------------------------------------


@pytest.mark.django_db
class TestMilestoneServices:
    def test_create_milestone(self, project):
        m = services.create_milestone(project, name="Beta", due_date="2025-06-30")
        assert m.pk is not None
        assert not m.is_completed

    def test_complete_milestone(self, milestone):
        completed = services.complete_milestone(milestone)
        assert completed.is_completed
        assert completed.completed_at is not None

    def test_delete_milestone(self, milestone):
        m_id = milestone.pk
        services.delete_milestone(milestone)
        assert not Milestone.objects.filter(pk=m_id).exists()


# ---------------------------------------------------------------------------
# Selectors
# ---------------------------------------------------------------------------


@pytest.mark.django_db
class TestProjectSelectors:
    def test_projects_for_tenant(self, project, tenant_acme):
        qs = selectors.projects_for_tenant(tenant_id=tenant_acme.pk)
        assert qs.filter(pk=project.pk).exists()

    def test_projects_by_status(self, project, tenant_acme):
        qs = selectors.projects_by_status(
            tenant_id=tenant_acme.pk, status=ProjectStatus.ACTIVE
        )
        assert qs.filter(pk=project.pk).exists()

    def test_project_by_public_id(self, project, tenant_acme):
        found = selectors.project_by_public_id(
            tenant_id=tenant_acme.pk, public_id=project.public_id
        )
        assert found.pk == project.pk

    def test_tags_for_tenant(self, proj_tag, tenant_acme):
        tags = selectors.tags_for_tenant(tenant_id=tenant_acme.pk)
        assert tags.filter(pk=proj_tag.pk).exists()

    def test_phases_for_project(self, phase, project):
        phases = selectors.phases_for_project(project_id=project.pk)
        assert phases.filter(pk=phase.pk).exists()

    def test_milestones_for_project(self, milestone, project):
        milestones = selectors.milestones_for_project(project_id=project.pk)
        assert milestones.filter(pk=milestone.pk).exists()


# ---------------------------------------------------------------------------
# Permissions registered
# ---------------------------------------------------------------------------


@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))
    expected = {
        "projects.project.view",
        "projects.project.create",
        "projects.project.update",
        "projects.project.delete",
        "projects.member.manage",
        "projects.phase.manage",
        "projects.milestone.manage",
    }
    assert expected.issubset(codes)


# ---------------------------------------------------------------------------
# API tests
# ---------------------------------------------------------------------------


@pytest.mark.django_db
class TestProjectsAPI:
    def _auth(self, client, alice, alice_admin_membership):
        client.force_login(alice)

    def test_list_projects(self, client, alice, alice_admin_membership, project):
        client.force_login(alice)
        resp = client.get("/api/v1/projects/")
        assert resp.status_code == 200
        data = resp.json()
        ids = [p["public_id"] for p in data]
        assert str(project.public_id) in ids

    def test_create_project(self, client, alice, alice_admin_membership, acme_tree):
        client.force_login(alice)
        resp = client.post(
            "/api/v1/projects/",
            data={
                "name": "Beta",
                "code": "BETA",
                "organization_node_id": acme_tree["root"].pk,
            },
            content_type="application/json",
        )
        assert resp.status_code == 201
        assert resp.json()["name"] == "Beta"

    def test_get_project_detail(self, client, alice, alice_admin_membership, project):
        client.force_login(alice)
        resp = client.get(f"/api/v1/projects/{project.public_id}/")
        assert resp.status_code == 200
        assert resp.json()["public_id"] == str(project.public_id)

    def test_update_project(self, client, alice, alice_admin_membership, project):
        client.force_login(alice)
        resp = client.patch(
            f"/api/v1/projects/{project.public_id}/",
            data={"name": "Alpha v2"},
            content_type="application/json",
        )
        assert resp.status_code == 200
        assert resp.json()["name"] == "Alpha v2"

    def test_delete_project(self, client, alice, alice_admin_membership, project):
        client.force_login(alice)
        resp = client.delete(f"/api/v1/projects/{project.public_id}/")
        assert resp.status_code == 204

    def test_change_status(self, client, alice, alice_admin_membership, project):
        client.force_login(alice)
        resp = client.post(
            f"/api/v1/projects/{project.public_id}/change-status/",
            data={"status": "completed"},
            content_type="application/json",
        )
        assert resp.status_code == 200
        assert resp.json()["status"] == "completed"

    def test_add_member(self, client, alice, alice_admin_membership, project):
        client.force_login(alice)
        resp = client.post(
            f"/api/v1/projects/{project.public_id}/members/",
            data={"user_id": alice.pk, "role": "lead"},
            content_type="application/json",
        )
        assert resp.status_code == 201

    def test_list_tags(self, client, alice, alice_admin_membership, proj_tag):
        client.force_login(alice)
        resp = client.get("/api/v1/projects/tags/")
        assert resp.status_code == 200

    def test_create_tag_api(self, client, alice, alice_admin_membership):
        client.force_login(alice)
        resp = client.post(
            "/api/v1/projects/tags/",
            data={"name": "Urgent", "color": "#ff0000"},
            content_type="application/json",
        )
        assert resp.status_code == 201
        assert resp.json()["name"] == "Urgent"

    def test_create_phase_api(self, client, alice, alice_admin_membership, project):
        client.force_login(alice)
        resp = client.post(
            f"/api/v1/projects/{project.public_id}/phases/",
            data={"name": "Sprint 1", "order": 1},
            content_type="application/json",
        )
        assert resp.status_code == 201
        assert resp.json()["name"] == "Sprint 1"

    def test_create_milestone_api(self, client, alice, alice_admin_membership, project):
        client.force_login(alice)
        resp = client.post(
            f"/api/v1/projects/{project.public_id}/milestones/",
            data={"name": "Go Live", "due_date": "2025-09-30"},
            content_type="application/json",
        )
        assert resp.status_code == 201
        assert resp.json()["name"] == "Go Live"

    def test_complete_milestone_api(
        self, client, alice, alice_admin_membership, project, milestone
    ):
        client.force_login(alice)
        resp = client.post(
            f"/api/v1/projects/{project.public_id}/milestones/{milestone.pk}/complete/",
            content_type="application/json",
        )
        assert resp.status_code == 200
        assert resp.json()["is_completed"] is True

    def test_unauthenticated_access(self, client, project):
        resp = client.get("/api/v1/projects/")
        assert resp.status_code in (401, 403)
