package com.njcn.rdms.module.project.framework.security.aop;

import com.njcn.rdms.framework.common.exception.ServiceException;
import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest;
import com.njcn.rdms.module.project.constant.ProjectExecutionConstants;
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
import com.njcn.rdms.module.project.constant.ProjectTaskConstants;
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee.ExecutionAssigneeLogPageReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.assignee.TaskAssigneeLogPageReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogPageReqVO;
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
import com.njcn.rdms.module.project.framework.security.service.ObjectPermissionService;
import com.njcn.rdms.module.project.service.project.execution.ProjectExecutionAssigneeService;
import com.njcn.rdms.module.project.service.project.execution.ProjectExecutionAssigneeServiceImpl;
import com.njcn.rdms.module.project.service.project.task.assignee.TaskAssigneeService;
import com.njcn.rdms.module.project.service.project.task.assignee.TaskAssigneeServiceImpl;
import com.njcn.rdms.module.project.service.project.task.worklog.TaskWorklogService;
import com.njcn.rdms.module.project.service.project.task.worklog.TaskWorklogServiceImpl;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.springframework.aop.aspectj.annotation.AspectJProxyFactory;

import java.util.List;

import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * 对象域内「协办人 / 工时」读路径的对象级鉴权回归测试（TD-001）。
 * <p>
 * 验证执行协办人、任务协办人、任务工时这 5 个读接口都已挂
 * {@link com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission}，且：
 * <ul>
 *   <li>{@code objectId="#projectId"} 解析为 projectId（而非 executionId/taskId）；</li>
 *   <li>permission 为同层 QUERY 码（执行域 project:execution:query、任务域 project:task:query），
 *       且因挂了 {@code visibilityFallback=true} 走可见性兜底通道 {@code checkPermissionOrVisibility}（而非 checkPermission）；</li>
 *   <li>无权（checkPermissionOrVisibility 抛异常）时方法体被拦、不执行。</li>
 * </ul>
 * 沿用 {@link ObjectPermissionAspectTest} 的 {@link AspectJProxyFactory} 手动织入方式，纯单测、不连库、不起容器。
 * 真实的「有无权限」判定逻辑由 ProjectObjectPermissionServiceTest 覆盖，本类不重复。
 */
class AssigneeWorklogReadPermissionTest extends BaseMockitoUnitTest {

    private static final Long PROJECT_ID = 1001L;
    private static final Long EXECUTION_ID = 2001L;
    private static final Long TASK_ID = 3001L;

    @Mock
    private ObjectPermissionService objectPermissionService;

    @Test
    void getExecutionAssigneeList_shouldGuardByExecutionQueryOnProjectId() {
        ProjectExecutionAssigneeService proxy =
                (ProjectExecutionAssigneeService) proxyOf(new ProjectExecutionAssigneeServiceImpl());
        denyPermission(ProjectExecutionConstants.PERMISSION_QUERY);

        assertThrows(ServiceException.class, () -> proxy.getExecutionAssigneeList(PROJECT_ID, EXECUTION_ID));

        verify(objectPermissionService).checkPermissionOrVisibility(PROJECT_ID, ProjectExecutionConstants.PERMISSION_QUERY);
    }

    @Test
    void getExecutionAssigneeLogPage_shouldGuardByExecutionQueryOnProjectId() {
        ProjectExecutionAssigneeService proxy =
                (ProjectExecutionAssigneeService) proxyOf(new ProjectExecutionAssigneeServiceImpl());
        denyPermission(ProjectExecutionConstants.PERMISSION_QUERY);

        assertThrows(ServiceException.class,
                () -> proxy.getExecutionAssigneeLogPage(PROJECT_ID, EXECUTION_ID, new ExecutionAssigneeLogPageReqVO()));

        verify(objectPermissionService).checkPermissionOrVisibility(PROJECT_ID, ProjectExecutionConstants.PERMISSION_QUERY);
    }

    @Test
    void getAssigneeList_shouldGuardByTaskQueryOnProjectId() {
        TaskAssigneeService proxy = (TaskAssigneeService) proxyOf(new TaskAssigneeServiceImpl());
        denyPermission(ProjectTaskConstants.PERMISSION_QUERY);

        assertThrows(ServiceException.class, () -> proxy.getAssigneeList(PROJECT_ID, EXECUTION_ID, TASK_ID));

        verify(objectPermissionService).checkPermissionOrVisibility(PROJECT_ID, ProjectTaskConstants.PERMISSION_QUERY);
    }

    @Test
    void getAssigneeLogPage_shouldGuardByTaskQueryOnProjectId() {
        TaskAssigneeService proxy = (TaskAssigneeService) proxyOf(new TaskAssigneeServiceImpl());
        denyPermission(ProjectTaskConstants.PERMISSION_QUERY);

        assertThrows(ServiceException.class,
                () -> proxy.getAssigneeLogPage(PROJECT_ID, EXECUTION_ID, TASK_ID, new TaskAssigneeLogPageReqVO()));

        verify(objectPermissionService).checkPermissionOrVisibility(PROJECT_ID, ProjectTaskConstants.PERMISSION_QUERY);
    }

    @Test
    void getWorklogPage_shouldGuardByTaskQueryOnProjectId() {
        TaskWorklogService proxy = (TaskWorklogService) proxyOf(new TaskWorklogServiceImpl());
        denyPermission(ProjectTaskConstants.PERMISSION_QUERY);

        assertThrows(ServiceException.class,
                () -> proxy.getWorklogPage(PROJECT_ID, EXECUTION_ID, TASK_ID, new TaskWorklogPageReqVO()));

        verify(objectPermissionService).checkPermissionOrVisibility(PROJECT_ID, ProjectTaskConstants.PERMISSION_QUERY);
    }

    /**
     * 用 AspectJProxyFactory 把对象权限切面织到真实 Service 上（JDK 代理，返回接口）。
     * target 为无依赖 new 出的实例：无权时切面在 proceed() 之前拦截，方法体不执行，故依赖为 null 无碍。
     */
    private Object proxyOf(Object target) {
        when(objectPermissionService.getObjectType()).thenReturn(ProjectObjectConstants.OBJECT_TYPE);
        AspectJProxyFactory proxyFactory = new AspectJProxyFactory(target);
        proxyFactory.addAspect(new ObjectPermissionAspect(List.of(objectPermissionService)));
        return proxyFactory.getProxy();
    }

    /**
     * 模拟「无权」：让对象权限校验抛出权限不足异常（与 ProjectObjectPermissionService 无权时一致）。
     */
    private void denyPermission(String permission) {
        doThrow(exception(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED))
                .when(objectPermissionService).checkPermissionOrVisibility(eq(PROJECT_ID), eq(permission));
    }

}
