"""
PM Module — Advanced Features Tests.

تست‌های واحد و یکپارچه برای فیچرهای پیشرفته ماژول مدیریت پروژه:
- Domain Services (ResourceLeveling, MonteCarlo, CCPM, ScheduleHealth)
- Advanced Entities (ActivityCodeType, UDFDefinition, etc.)
- Advanced API Endpoints
"""
import uuid
from datetime import date, timedelta
from decimal import Decimal

import pytest

from modules.pm.backend.domain.entities.task import Task, Dependency, TaskAssignment
from modules.pm.backend.domain.entities.advanced import (
    ActivityCodeType, ActivityCodeValue, TaskActivityCode,
    UDFDefinition, UDFValue,
    ResourceSkill, ResourceAvailability, ResourceCostRate,
    CostAccount, FundingSource, CashFlowEntry,
    ScheduleHealthSnapshot, ProjectTemplate,
)
from modules.pm.backend.domain.value_objects.common import (
    TaskStatus, TaskType, DependencyType, TaskAssignmentRole,
)
from modules.pm.backend.domain.entities.resource import ResourceAssignment
from modules.pm.backend.domain.services.advanced import (
    ResourceLevelingService, MonteCarloService,
    CCPMService, ScheduleHealthService,
    TaskEstimate, LevelingResult, MonteCarloResult, CCPMResult, BufferInfo,
)
from modules.pm.backend.domain.services.scheduling import CalendarData


# ═══════════════════════════════════════════════════════════
# Helpers
# ═══════════════════════════════════════════════════════════

def _tid():
    return uuid.uuid4()


def _make_task(project_id=None, **kwargs):
    defaults = {
        "id": _tid(),
        "tenant_id": _tid(),
        "project_id": project_id or _tid(),
        "code": "T",
        "title": "Task",
        "task_type": TaskType.TASK,
        "status": TaskStatus.NOT_STARTED,
        "planned_start": date(2026, 3, 1),  # Sunday — use fixed date, not date.today()
        "planned_end": date(2026, 3, 5),
        "duration": 5,
    }
    defaults.update(kwargs)
    return Task(**defaults)


# ═══════════════════════════════════════════════════════════
# 1) Advanced Entity Construction
# ═══════════════════════════════════════════════════════════

class TestAdvancedEntities:
    """تست ساخت entity‌های پیشرفته."""

    def test_activity_code_type(self):
        e = ActivityCodeType(
            id=_tid(), tenant_id=_tid(),
            name="Phase", code="PH", scope="global",
        )
        assert e.name == "Phase"
        assert e.is_active is True

    def test_activity_code_value(self):
        e = ActivityCodeValue(
            id=_tid(), tenant_id=_tid(),
            code_type_id=_tid(), code="DESIGN", name="Design Phase",
        )
        assert e.code == "DESIGN"
        assert e.parent_id is None

    def test_udf_definition(self):
        e = UDFDefinition(
            id=_tid(), tenant_id=_tid(),
            name="Custom Field", code="cf_1",
            field_type="text", entity_type="task",
        )
        assert e.field_type == "text"
        assert e.is_required is False
        assert e.is_active is True

    def test_udf_value(self):
        e = UDFValue(
            id=_tid(), tenant_id=_tid(),
            definition_id=_tid(), entity_id=_tid(),
            entity_type="task", value_text="Test Value",
        )
        assert e.value_text == "Test Value"
        assert e.value_number is None

    def test_resource_skill(self):
        e = ResourceSkill(
            id=_tid(), tenant_id=_tid(),
            resource_id=_tid(), skill_name="Python",
            proficiency_level=5,
        )
        assert e.skill_name == "Python"
        assert e.proficiency_level == 5

    def test_resource_availability(self):
        e = ResourceAvailability(
            id=_tid(), tenant_id=_tid(),
            resource_id=_tid(),
            start_date=date(2026, 1, 1),
            end_date=date(2026, 6, 30),
            available_units=Decimal("80"),
        )
        assert e.available_units == Decimal("80")

    def test_resource_cost_rate(self):
        e = ResourceCostRate(
            id=_tid(), tenant_id=_tid(),
            resource_id=_tid(),
            effective_date=date(2026, 1, 1),
            standard_rate=Decimal("500000"),
            overtime_rate=Decimal("750000"),
        )
        assert e.standard_rate == Decimal("500000")

    def test_cost_account(self):
        e = CostAccount(
            id=_tid(), tenant_id=_tid(),
            project_id=_tid(), code="CBS-01", name="Labor",
            budget_amount=Decimal("100000000"),
            actual_amount=Decimal("45000000"),
        )
        assert e.budget_amount - e.actual_amount == Decimal("55000000")

    def test_funding_source(self):
        e = FundingSource(
            id=_tid(), tenant_id=_tid(),
            project_id=_tid(), name="Bank Loan",
            total_amount=Decimal("500000000"),
            allocated_amount=Decimal("100000000"),
            remaining_amount=Decimal("400000000"),
        )
        assert e.remaining_amount == e.total_amount - e.allocated_amount

    def test_cash_flow_entry(self):
        e = CashFlowEntry(
            id=_tid(), tenant_id=_tid(),
            project_id=_tid(),
            period_start=date(2026, 1, 1),
            period_end=date(2026, 1, 31),
            planned_income=Decimal("200000000"),
            planned_expense=Decimal("150000000"),
            actual_income=Decimal("180000000"),
            actual_expense=Decimal("160000000"),
        )
        assert e.planned_income - e.planned_expense == Decimal("50000000")

    def test_schedule_health_snapshot(self):
        e = ScheduleHealthSnapshot(
            id=_tid(), tenant_id=_tid(),
            project_id=_tid(),
            snapshot_date=date(2026, 3, 1),
            health_score=85,
            total_tasks_count=100,
        )
        assert e.health_score == 85

    def test_project_template(self):
        e = ProjectTemplate(
            id=_tid(), tenant_id=_tid(),
            name="Construction Template",
            category="construction",
            template_data={"tasks": [], "dependencies": []},
        )
        assert e.is_active is True
        assert e.usage_count == 0

    def test_task_assignment(self):
        e = TaskAssignment(
            id=_tid(), tenant_id=_tid(),
            task_id=_tid(), user_id=_tid(),
            assignment_role=TaskAssignmentRole.PRIMARY,
        )
        assert e.assignment_role == TaskAssignmentRole.PRIMARY
        assert e.is_active is True


# ═══════════════════════════════════════════════════════════
# 2) Resource Leveling Service
# ═══════════════════════════════════════════════════════════

class TestResourceLevelingService:
    """تست تسطیح منابع."""

    def test_no_overallocation(self):
        """اگر تخصیص‌ها بیشینه‌ای نداشته باشند، تسطیح تغییری نمی‌دهد."""
        pid = _tid()
        t = _make_task(project_id=pid, planned_start=date(2026, 3, 2), duration=3)
        res_id = _tid()
        assign = ResourceAssignment(
            id=_tid(), tenant_id=t.tenant_id,
            project_id=pid, task_id=t.id, resource_id=res_id,
            units=Decimal("50"),
        )
        result = ResourceLevelingService.level(
            tasks=[t],
            dependencies=[],
            assignments=[assign],
            resource_max_units={res_id: Decimal("100")},
        )
        assert result.success is True
        assert len(result.overallocated_resources) == 0

    def test_overallocation_detected(self):
        """تشخیص تخصیص بیش‌ازحد منبع."""
        pid = _tid()
        tid = _tid()
        res_id = _tid()

        t1 = _make_task(project_id=pid, planned_start=date(2026, 3, 2), duration=5)
        t2 = _make_task(project_id=pid, planned_start=date(2026, 3, 2), duration=3)

        a1 = ResourceAssignment(
            id=_tid(), tenant_id=t1.tenant_id,
            project_id=pid, task_id=t1.id, resource_id=res_id,
            units=Decimal("80"),
        )
        a2 = ResourceAssignment(
            id=_tid(), tenant_id=t2.tenant_id,
            project_id=pid, task_id=t2.id, resource_id=res_id,
            units=Decimal("60"),
        )

        result = ResourceLevelingService.level(
            tasks=[t1, t2],
            dependencies=[],
            assignments=[a1, a2],
            resource_max_units={res_id: Decimal("100")},
        )
        # Either resolves overallocation or reports it
        assert isinstance(result, LevelingResult)

    def test_empty_tasks(self):
        result = ResourceLevelingService.level(
            tasks=[], dependencies=[], assignments={}, resource_max_units={},
        )
        assert result.success is True


# ═══════════════════════════════════════════════════════════
# 3) Monte Carlo Service
# ═══════════════════════════════════════════════════════════

class TestMonteCarloService:
    """تست شبیه‌سازی مونت‌کارلو."""

    def test_basic_simulation(self):
        """شبیه‌سازی ساده با یک تسک."""
        pid = _tid()
        t1 = _make_task(project_id=pid, id=(_t1_id := _tid()), duration=5)

        estimates = [
            TaskEstimate(task_id=_t1_id, optimistic=3, most_likely=5, pessimistic=10),
        ]

        result = MonteCarloService.simulate(
            tasks=[t1],
            dependencies=[],
            estimates=estimates,
            iterations=500,
        )
        assert isinstance(result, MonteCarloResult)
        assert result.mean_duration > 0
        assert result.p50_duration > 0
        assert result.p80_duration >= result.p50_duration
        assert result.p90_duration >= result.p80_duration
        assert result.p95_duration >= result.p90_duration
        assert len(result.histogram) > 0
        assert len(result.confidence_range) == 2

    def test_two_sequential_tasks(self):
        """شبیه‌سازی با دو تسک متوالی."""
        pid = _tid()
        t1 = _make_task(project_id=pid, id=(_t1_id := _tid()), duration=5)
        t2 = _make_task(project_id=pid, id=(_t2_id := _tid()), duration=3)

        dep = Dependency(
            id=_tid(), tenant_id=t1.tenant_id, project_id=pid,
            predecessor_id=_t1_id, successor_id=_t2_id,
            dependency_type=DependencyType.FS, lag_days=0,
        )

        estimates = [
            TaskEstimate(task_id=_t1_id, optimistic=3, most_likely=5, pessimistic=10),
            TaskEstimate(task_id=_t2_id, optimistic=2, most_likely=3, pessimistic=7),
        ]

        result = MonteCarloService.simulate(
            tasks=[t1, t2],
            dependencies=[dep],
            estimates=estimates,
            iterations=500,
        )
        # Sum of two PERT distributions
        assert result.mean_duration >= 5  # At least the sum of most_likely

    def test_empty_tasks(self):
        result = MonteCarloService.simulate([], [], [], iterations=100)
        assert result.mean_duration == 0.0


# ═══════════════════════════════════════════════════════════
# 4) Schedule Health Service
# ═══════════════════════════════════════════════════════════

class TestScheduleHealthService:
    """تست ارزیابی سلامت زمان‌بندی."""

    def test_healthy_schedule(self):
        """زمان‌بندی سالم — تسک‌ها با ارتباط و تاریخ معتبر."""
        pid = _tid()
        tid1 = _tid()
        tid2 = _tid()
        tenant_id = _tid()

        t1 = _make_task(
            project_id=pid, id=tid1, tenant_id=tenant_id,
            planned_start=date(2026, 3, 2), planned_end=date(2026, 3, 6),
            duration=5, total_float=0,
        )
        t2 = _make_task(
            project_id=pid, id=tid2, tenant_id=tenant_id,
            planned_start=date(2026, 3, 9), planned_end=date(2026, 3, 13),
            duration=5, total_float=0,
        )

        dep = Dependency(
            id=_tid(), tenant_id=tenant_id, project_id=pid,
            predecessor_id=tid1, successor_id=tid2,
            dependency_type=DependencyType.FS, lag_days=0,
        )

        res_id = _tid()
        a1 = ResourceAssignment(
            id=_tid(), tenant_id=tenant_id,
            project_id=pid, task_id=tid1, resource_id=res_id,
        )
        a2 = ResourceAssignment(
            id=_tid(), tenant_id=tenant_id,
            project_id=pid, task_id=tid2, resource_id=res_id,
        )

        snapshot = ScheduleHealthService.assess(
            tasks=[t1, t2],
            dependencies=[dep],
            assignments=[a1, a2],
        )
        assert isinstance(snapshot, ScheduleHealthSnapshot)
        assert snapshot.health_score >= 0
        assert snapshot.total_tasks_count == 2
        assert snapshot.missing_logic_count == 0

    def test_missing_logic(self):
        """تسک بدون ارتباط → missing logic."""
        pid = _tid()
        t1 = _make_task(project_id=pid)
        t2 = _make_task(project_id=pid)
        t3 = _make_task(project_id=pid)

        # Only 1 dependency between t1 and t2; t3 has no link
        dep = Dependency(
            id=_tid(), tenant_id=t1.tenant_id, project_id=pid,
            predecessor_id=t1.id, successor_id=t2.id,
            dependency_type=DependencyType.FS, lag_days=0,
        )

        snapshot = ScheduleHealthService.assess(
            tasks=[t1, t2, t3],
            dependencies=[dep],
            assignments=[],
        )
        assert snapshot.missing_logic_count >= 1

    def test_empty_schedule(self):
        snapshot = ScheduleHealthService.assess(
            tasks=[], dependencies=[], assignments=[],
        )
        assert snapshot.health_score == 100
        assert snapshot.total_tasks_count == 0

    def test_negative_float_detected(self):
        """تشخیص total_float منفی."""
        pid = _tid()
        t = _make_task(project_id=pid, total_float=-3)

        snapshot = ScheduleHealthService.assess(
            tasks=[t], dependencies=[], assignments=[],
        )
        assert snapshot.negative_float_count >= 1

    def test_high_duration_detected(self):
        """تشخیص مدت‌زمان بالا (> 44 روز)."""
        pid = _tid()
        t = _make_task(project_id=pid, duration=60)

        snapshot = ScheduleHealthService.assess(
            tasks=[t], dependencies=[], assignments=[],
        )
        assert snapshot.details.get("high_duration", {}).get("count", 0) >= 1


# ═══════════════════════════════════════════════════════════
# 5) CCPM Service
# ═══════════════════════════════════════════════════════════

class TestCCPMService:
    """تست Critical Chain Project Management."""

    def test_single_chain(self):
        """زنجیره بحرانی تک‌مسیره."""
        pid = _tid()
        t1 = _make_task(project_id=pid, id=(_t1 := _tid()), duration=10)
        t2 = _make_task(project_id=pid, id=(_t2 := _tid()), duration=6)

        dep = Dependency(
            id=_tid(), tenant_id=t1.tenant_id, project_id=pid,
            predecessor_id=_t1, successor_id=_t2,
            dependency_type=DependencyType.FS, lag_days=0,
        )

        result = CCPMService.calculate(
            tasks=[t1, t2],
            dependencies=[dep],
            critical_path_ids=[_t1, _t2],
        )
        assert isinstance(result, CCPMResult)
        assert len(result.critical_chain) >= 1
        assert result.project_buffer.size_days > 0
        assert result.total_project_duration > 0

    def test_empty_chain(self):
        result = CCPMService.calculate(tasks=[], dependencies=[], critical_path_ids=[])
        assert result.project_buffer.size_days == 0
        assert len(result.critical_chain) == 0

    def test_buffer_consumption_green(self):
        """مصرف بافر سبز."""
        buf = BufferInfo(buffer_type="project", size_days=20, consumed_days=3)
        result = CCPMService.calculate_buffer_consumption(buf, 50)
        assert result.status in ("green", "yellow", "red")

    def test_buffer_consumption_values(self):
        """حساب‌های مختلف مصرف بافر."""
        # Low consumption relative to progress → green
        buf = BufferInfo(buffer_type="project", size_days=100, consumed_days=10)
        result = CCPMService.calculate_buffer_consumption(buf, 50)
        assert result.status == "green"

        # High consumption relative to progress → red
        buf2 = BufferInfo(buffer_type="project", size_days=100, consumed_days=80)
        result2 = CCPMService.calculate_buffer_consumption(buf2, 20)
        assert result2.status == "red"


# ═══════════════════════════════════════════════════════════
# 6) Advanced API Tests (Integration)
# ═══════════════════════════════════════════════════════════

@pytest.mark.django_db(transaction=True)
class TestAdvancedAPIEndpoints:
    """تست‌های یکپارچه API — فقط بررسی دسترسی‌پذیری endpoint‌ها."""

    @pytest.fixture(autouse=True)
    def setup_client(self, auth_client):
        """auth_client fixture از conftest — admin user با دسترسی کامل."""
        self.client = auth_client

    def test_activity_code_types_list(self):
        response = self.client.get("/api/v1/pm/activity-code-types/")
        assert response.status_code in (200, 401, 403)

    def test_udf_definitions_list(self):
        response = self.client.get("/api/v1/pm/udf-definitions/")
        assert response.status_code in (200, 401, 403)

    def test_resource_skills_list(self):
        response = self.client.get("/api/v1/pm/resource-skills/")
        assert response.status_code in (200, 401, 403)

    def test_cost_accounts_list(self):
        response = self.client.get("/api/v1/pm/cost-accounts/")
        assert response.status_code in (200, 401, 403)

    def test_funding_sources_list(self):
        response = self.client.get("/api/v1/pm/funding-sources/")
        assert response.status_code in (200, 401, 403)

    def test_cash_flow_list(self):
        response = self.client.get("/api/v1/pm/cash-flow/")
        assert response.status_code in (200, 401, 403)

    def test_schedule_health_list(self):
        response = self.client.get("/api/v1/pm/schedule-health/")
        assert response.status_code in (200, 401, 403)

    def test_project_templates_list(self):
        response = self.client.get("/api/v1/pm/project-templates/")
        assert response.status_code in (200, 401, 403)
