"""S3.3 — Time Tracking backend tests.

Covers:
- TimeEntry model + service (create/update/delete)
- Daily hours limit enforcement
- Approve service (single + bulk)
- Selectors: filter_entries, my_weekly_entries, project_time_report
- Events dispatched on create, approve
- Permission codes registered
- API: entry list, create, detail, update, delete
- API: my weekly, bulk-approve
- API: project time report
- Tenant isolation
"""

from __future__ import annotations

from datetime import date, timedelta
from decimal import Decimal

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, ProjectStatus
from simorgh.apps.tasks.models import Task, TaskStatus, TaskPriority
from simorgh.apps.time_tracking.models import TimeEntry
from simorgh.apps.time_tracking 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 task(tenant_acme, acme_tree, alice, project):
    return Task.objects.create(
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
        title="Implement feature",
        reporter=alice,
        project=project,
        status=TaskStatus.TODO,
        priority=TaskPriority.MEDIUM,
    )


@pytest.fixture
def entry(tenant_acme, acme_tree, alice, project):
    return TimeEntry.objects.create(
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
        user=alice,
        project=project,
        date=date(2026, 1, 10),
        hours=Decimal("3.00"),
        description="Working on Alpha",
        is_billable=True,
    )


# ---------------------------------------------------------------------------
# Service tests
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestTimeEntryServices:

    def test_create_entry(self, tenant_acme, acme_tree, alice, project):
        e = services.create_time_entry(
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
            user_id=alice.pk,
            project_id=project.pk,
            date=date(2026, 1, 15),
            hours=Decimal("4.00"),
            description="Dev work",
            is_billable=True,
        )
        assert e.pk is not None
        assert e.hours == Decimal("4.00")
        assert e.is_approved is False

    def test_create_with_task(self, tenant_acme, acme_tree, alice, project, task):
        e = services.create_time_entry(
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
            user_id=alice.pk,
            project_id=project.pk,
            date=date(2026, 1, 16),
            hours=Decimal("2.50"),
            task_id=task.pk,
        )
        assert e.task_id == task.pk

    def test_create_zero_hours_raises(self, tenant_acme, acme_tree, alice, project):
        with pytest.raises(ValueError, match="greater than zero"):
            services.create_time_entry(
                tenant_id=tenant_acme.pk,
                organization_node_id=acme_tree["root"].pk,
                user_id=alice.pk,
                project_id=project.pk,
                date=date(2026, 1, 17),
                hours=Decimal("0"),
            )

    def test_daily_limit_exceeded(self, tenant_acme, acme_tree, alice, project):
        services.create_time_entry(
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
            user_id=alice.pk,
            project_id=project.pk,
            date=date(2026, 1, 18),
            hours=Decimal("20.00"),
        )
        with pytest.raises(ValueError, match="Exceeded daily maximum"):
            services.create_time_entry(
                tenant_id=tenant_acme.pk,
                organization_node_id=acme_tree["root"].pk,
                user_id=alice.pk,
                project_id=project.pk,
                date=date(2026, 1, 18),
                hours=Decimal("5.00"),
            )

    def test_update_entry(self, entry):
        services.update_time_entry(entry, description="Updated description", hours=Decimal("5.00"))
        entry.refresh_from_db()
        assert entry.description == "Updated description"
        assert entry.hours == Decimal("5.00")

    def test_update_approved_entry_raises(self, entry):
        entry.is_approved = True
        entry.save()
        with pytest.raises(ValueError, match="approved"):
            services.update_time_entry(entry, hours=Decimal("6.00"))

    def test_delete_entry(self, entry):
        pk = entry.pk
        services.delete_time_entry(entry)
        assert not TimeEntry.objects.filter(pk=pk).exists()

    def test_delete_approved_entry_raises(self, entry):
        entry.is_approved = True
        entry.save()
        with pytest.raises(ValueError, match="approved"):
            services.delete_time_entry(entry)

    def test_approve_entries(self, tenant_acme, entry, alice):
        count = services.approve_time_entries([entry.pk], approved_by_id=alice.pk, tenant_id=tenant_acme.pk)
        assert count == 1
        entry.refresh_from_db()
        assert entry.is_approved is True
        assert entry.approved_by_id == alice.pk

    def test_approve_already_approved_skipped(self, tenant_acme, entry, alice):
        entry.is_approved = True
        entry.save()
        count = services.approve_time_entries([entry.pk], approved_by_id=alice.pk, tenant_id=tenant_acme.pk)
        assert count == 0


# ---------------------------------------------------------------------------
# Selector tests
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestTimeEntrySelectors:

    def test_filter_by_user(self, tenant_acme, entry, alice):
        from simorgh.apps.accounts.models import User
        bob = User.objects.create_user("+989000000099", password="x", email="bob@example.com")
        qs = selectors.filter_entries(tenant_acme.pk, user_id=alice.pk)
        assert entry in qs
        qs2 = selectors.filter_entries(tenant_acme.pk, user_id=bob.pk)
        assert entry not in qs2

    def test_filter_by_project(self, tenant_acme, entry, project):
        qs = selectors.filter_entries(tenant_acme.pk, project_id=project.pk)
        assert entry in qs

    def test_filter_by_date_range(self, tenant_acme, entry):
        qs = selectors.filter_entries(
            tenant_acme.pk,
            date_from=date(2026, 1, 9),
            date_to=date(2026, 1, 11),
        )
        assert entry in qs
        qs2 = selectors.filter_entries(
            tenant_acme.pk,
            date_from=date(2026, 1, 11),
            date_to=date(2026, 1, 12),
        )
        assert entry not in qs2

    def test_filter_by_billable(self, tenant_acme, entry):
        qs = selectors.filter_entries(tenant_acme.pk, is_billable=True)
        assert entry in qs
        qs2 = selectors.filter_entries(tenant_acme.pk, is_billable=False)
        assert entry not in qs2

    def test_filter_by_approved(self, tenant_acme, entry):
        qs = selectors.filter_entries(tenant_acme.pk, is_approved=False)
        assert entry in qs

    def test_my_weekly(self, tenant_acme, entry, alice):
        week_start = date(2026, 1, 5)   # Mon
        week_end   = date(2026, 1, 11)  # Sun (entry is Jan 10)
        qs = selectors.my_weekly_entries(tenant_acme.pk, alice.pk, week_start, week_end)
        assert entry in qs

    def test_project_time_report(self, tenant_acme, project, entry):
        rows = list(selectors.project_time_report(tenant_acme.pk, project.pk, None, None))
        assert len(rows) == 1
        assert rows[0]["total_hours"] == Decimal("3.00")

    def test_entry_by_public_id(self, tenant_acme, entry):
        fetched = selectors.entry_by_public_id(tenant_acme.pk, str(entry.public_id))
        assert fetched.pk == entry.pk


# ---------------------------------------------------------------------------
# Permission codes
# ---------------------------------------------------------------------------

@pytest.mark.django_db
def test_permission_codes_registered(db):
    sync_registry_to_db()
    from simorgh.apps.iam.models import Permission
    codes = set(Permission.objects.values_list("codename", flat=True))
    for code in [
        "time.entry.create",
        "time.entry.update_own",
        "time.entry.manage_others",
        "time.report.view",
        "time.report.view_all",
    ]:
        assert code in codes, f"Missing permission: {code}"


# ---------------------------------------------------------------------------
# API helpers
# ---------------------------------------------------------------------------

def _auth_client(client, user, tenant_acme):
    from rest_framework.test import APIClient
    c = APIClient()
    c.force_login(user)
    c.credentials(HTTP_X_TENANT=tenant_acme.slug)
    return c


# ---------------------------------------------------------------------------
# API: entry list / create
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestTimeEntryListCreateAPI:

    def test_list_returns_entries(self, client, alice, tenant_acme, entry, alice_admin_membership):
        c = _auth_client(client, alice, tenant_acme)
        res = c.get("/api/v1/time-entries/")
        assert res.status_code == 200
        assert len(res.data) >= 1

    def test_create_entry(self, client, alice, tenant_acme, project, alice_admin_membership):
        c = _auth_client(client, alice, tenant_acme)
        res = c.post("/api/v1/time-entries/", {
            "project_id": project.pk,
            "date": "2026-03-01",
            "hours": "2.50",
            "description": "API test entry",
            "is_billable": True,
        }, format="json")
        assert res.status_code == 201
        assert res.data["hours"] == "2.50"

    def test_create_invalid_hours(self, client, alice, tenant_acme, project, alice_admin_membership):
        c = _auth_client(client, alice, tenant_acme)
        res = c.post("/api/v1/time-entries/", {
            "project_id": project.pk,
            "date": "2026-03-02",
            "hours": "0",
        }, format="json")
        assert res.status_code == 400

    def test_list_filter_by_project(self, client, alice, tenant_acme, entry, project, alice_admin_membership):
        c = _auth_client(client, alice, tenant_acme)
        res = c.get(f"/api/v1/time-entries/?project={project.pk}")
        assert res.status_code == 200
        assert all(e["project_id"] == project.pk for e in res.data)

    def test_list_requires_auth(self, client, tenant_acme):
        from rest_framework.test import APIClient
        c = APIClient()
        c.credentials(HTTP_X_TENANT=tenant_acme.slug)
        res = c.get("/api/v1/time-entries/")
        assert res.status_code == 401

    def test_requires_tenant(self, client):
        from rest_framework.test import APIClient
        from simorgh.apps.accounts.models import User
        # A user with no memberships → tenant cannot be resolved → 400
        nobody = User.objects.create_user("+989000000088", password="x", email="nobody@test.com")
        c = APIClient()
        c.force_login(nobody)
        res = c.get("/api/v1/time-entries/")
        assert res.status_code == 400


# ---------------------------------------------------------------------------
# API: entry detail (GET / PATCH / DELETE)
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestTimeEntryDetailAPI:

    def test_get_entry(self, client, alice, tenant_acme, entry, alice_admin_membership):
        c = _auth_client(client, alice, tenant_acme)
        res = c.get(f"/api/v1/time-entries/{entry.public_id}/")
        assert res.status_code == 200
        assert res.data["public_id"] == str(entry.public_id)

    def test_patch_entry(self, client, alice, tenant_acme, entry, alice_admin_membership):
        c = _auth_client(client, alice, tenant_acme)
        res = c.patch(f"/api/v1/time-entries/{entry.public_id}/", {
            "description": "Updated via API",
        }, format="json")
        assert res.status_code == 200
        assert res.data["description"] == "Updated via API"

    def test_delete_entry(self, client, alice, tenant_acme, entry, alice_admin_membership):
        c = _auth_client(client, alice, tenant_acme)
        res = c.delete(f"/api/v1/time-entries/{entry.public_id}/")
        assert res.status_code == 204

    def test_patch_approved_entry_returns_400(self, client, alice, tenant_acme, entry, alice_admin_membership):
        entry.is_approved = True
        entry.save()
        c = _auth_client(client, alice, tenant_acme)
        res = c.patch(f"/api/v1/time-entries/{entry.public_id}/", {"hours": "6.00"}, format="json")
        assert res.status_code == 400

    def test_delete_approved_entry_returns_400(self, client, alice, tenant_acme, entry, alice_admin_membership):
        entry.is_approved = True
        entry.save()
        c = _auth_client(client, alice, tenant_acme)
        res = c.delete(f"/api/v1/time-entries/{entry.public_id}/")
        assert res.status_code == 400

    def test_cross_tenant_isolation(self, client, alice, tenant_acme, tenant_globex, entry, alice_admin_membership):
        from rest_framework.test import APIClient
        c = APIClient()
        c.force_login(alice)
        c.credentials(HTTP_X_TENANT=tenant_globex.slug)
        res = c.get(f"/api/v1/time-entries/{entry.public_id}/")
        assert res.status_code == 404


# ---------------------------------------------------------------------------
# API: my weekly
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestMyWeeklyAPI:

    def test_my_weekly_default_week(self, client, alice, tenant_acme, entry, alice_admin_membership):
        c = _auth_client(client, alice, tenant_acme)
        # entry is Jan 10 2026 — explicitly pass the week
        res = c.get("/api/v1/time-entries/my/weekly/?week_start=2026-01-05")
        assert res.status_code == 200
        assert "week_start" in res.data
        assert "rows" in res.data

    def test_my_weekly_contains_entry(self, client, alice, tenant_acme, entry, alice_admin_membership):
        c = _auth_client(client, alice, tenant_acme)
        res = c.get("/api/v1/time-entries/my/weekly/?week_start=2026-01-05")
        assert res.status_code == 200
        all_entries = []
        for row in res.data["rows"]:
            all_entries.extend(row["entries"])
        pids = [e["public_id"] for e in all_entries]
        assert str(entry.public_id) in pids


# ---------------------------------------------------------------------------
# API: bulk approve
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestBulkApproveAPI:

    def test_bulk_approve(self, client, alice, tenant_acme, entry, alice_admin_membership):
        c = _auth_client(client, alice, tenant_acme)
        res = c.post("/api/v1/time-entries/bulk-approve/", {"ids": [entry.pk]}, format="json")
        assert res.status_code == 200
        assert res.data["approved"] == 1
        entry.refresh_from_db()
        assert entry.is_approved is True

    def test_bulk_approve_empty_list(self, client, alice, tenant_acme, alice_admin_membership):
        c = _auth_client(client, alice, tenant_acme)
        res = c.post("/api/v1/time-entries/bulk-approve/", {"ids": []}, format="json")
        assert res.status_code == 200
        assert res.data["approved"] == 0
