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

import com.njcn.rdms.framework.common.exception.ServiceException;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest;
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
import com.njcn.rdms.module.project.service.datascope.ObjectDataScope;
import com.njcn.rdms.module.project.service.datascope.ObjectDataScopeService;
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
import com.njcn.rdms.module.system.api.permission.dto.UserVisibilityConfigRespDTO;
import com.njcn.rdms.module.system.enums.permission.PermissionScopeTypeEnum;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;

import java.util.Collections;
import java.util.List;
import java.util.Set;

import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

class ProjectObjectPermissionServiceTest extends BaseMockitoUnitTest {

    @InjectMocks
    private ProjectObjectPermissionService permissionService;
    @Mock
    private UserObjectRoleMapper userObjectRoleMapper;
    @Mock
    private ObjectPermissionApi objectPermissionApi;
    @Mock
    private ProjectMapper projectMapper;
    @Mock
    private ObjectDataScopeService objectDataScopeService;
    @Mock
    private com.njcn.rdms.module.system.api.permission.UserVisibilityConfigApi userVisibilityConfigApi;

    @Test
    void checkPermission_whenMemberOnlyAndActiveMember_shouldPass() {
        Long projectId = 1001L;
        Long loginUserId = 2001L;
        when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
                .thenReturn(List.of(createMember(projectId, loginUserId, 3001L)));

        try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId)) {
            assertDoesNotThrow(() -> permissionService.checkPermission(projectId, null, true));
        }

        verifyNoInteractions(objectPermissionApi);
    }

    @Test
    void checkPermission_whenNoActiveMember_shouldThrowProjectPermissionDenied() {
        Long projectId = 1002L;
        Long loginUserId = 2002L;
        when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
                .thenReturn(Collections.emptyList());

        try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId)) {
            ServiceException ex = assertThrows(ServiceException.class,
                    () -> permissionService.checkPermission(projectId, "project:project:update", false));
            assertEquals(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED.getCode(), ex.getCode());
            // 用户可见 message 不得外泄权限码（技术细节走 log.warn）
            assertFalse(ex.getMessage().contains(":"), "message 不应外泄权限码");
        }
    }

    @Test
    void checkPermission_whenPermissionPresent_shouldPass() {
        Long projectId = 1003L;
        Long loginUserId = 2003L;
        when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
                .thenReturn(List.of(createMember(projectId, loginUserId, 3003L)));
        when(objectPermissionApi.getObjectRolePermissions(3003L,
                PermissionScopeTypeEnum.OBJECT.getScopeType(), "project"))
                .thenReturn(success(Set.of("project:project:update")));

        try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId)) {
            assertDoesNotThrow(() -> permissionService.checkPermission(projectId, "project:project:update", false));
        }
    }

    @Test
    void checkPermission_whenPermissionMissing_shouldThrowProjectPermissionDenied() {
        Long projectId = 1004L;
        Long loginUserId = 2004L;
        when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
                .thenReturn(List.of(createMember(projectId, loginUserId, 3004L)));
        when(objectPermissionApi.getObjectRolePermissions(3004L,
                PermissionScopeTypeEnum.OBJECT.getScopeType(), "project"))
                .thenReturn(success(Set.of("project:project:query")));

        try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId)) {
            ServiceException ex = assertThrows(ServiceException.class,
                    () -> permissionService.checkPermission(projectId, "project:project:delete", false));
            assertEquals(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED.getCode(), ex.getCode());
            assertFalse(ex.getMessage().contains(":"), "message 不应外泄权限码");
        }
    }

    @Test
    void checkPermission_whenMemberOnlyAndNoActiveMember_shouldThrowWithoutLeak() {
        Long projectId = 1005L;
        Long loginUserId = 2005L;
        when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
                .thenReturn(Collections.emptyList());

        try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId)) {
            ServiceException ex = assertThrows(ServiceException.class,
                    () -> permissionService.checkPermission(projectId, "project:project:query", true));
            assertEquals(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED.getCode(), ex.getCode());
            // memberOnly 分支原先会外泄字面 member 标记，现统一为友好文案
            assertFalse(ex.getMessage().contains("member"), "message 不应外泄 member 标记");
            assertFalse(ex.getMessage().contains(":"), "message 不应外泄权限码");
        }
    }

    @Test
    void checkAccessible_whenActiveMember_shouldPass() {
        Long projectId = 1101L;
        Long loginUserId = 2101L;
        when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
                .thenReturn(List.of(createMember(projectId, loginUserId, 3101L)));

        try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId)) {
            assertDoesNotThrow(() -> permissionService.checkAccessible(projectId));
        }
        // 显式成员提前放行，不触达对象查询与数据范围计算
        verifyNoInteractions(projectMapper, objectDataScopeService);
    }

    @Test
    void checkAccessible_whenImplicitObserverInScope_shouldPass() {
        Long projectId = 1102L;
        Long loginUserId = 2102L;
        when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
                .thenReturn(Collections.emptyList());
        ProjectDO project = new ProjectDO();
        project.setId(projectId);
        project.setDirectionCode("D-NET");
        when(projectMapper.selectById(projectId)).thenReturn(project);
        // 无显式角色，但方向 scope 命中 → 隐式 observer 放行
        when(objectDataScopeService.compute(loginUserId, "project"))
                .thenReturn(ObjectDataScope.idList(Collections.emptySet(), Set.of("D-NET")));

        try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId)) {
            assertDoesNotThrow(() -> permissionService.checkAccessible(projectId));
        }
    }

    @Test
    void checkAccessible_whenSuperAdminScopeAll_shouldPass() {
        Long projectId = 1103L;
        Long loginUserId = 2103L;
        when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
                .thenReturn(Collections.emptyList());
        ProjectDO project = new ProjectDO();
        project.setId(projectId);
        project.setDirectionCode("D-NET");
        when(projectMapper.selectById(projectId)).thenReturn(project);
        when(objectDataScopeService.compute(loginUserId, "project"))
                .thenReturn(ObjectDataScope.all());

        try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId)) {
            assertDoesNotThrow(() -> permissionService.checkAccessible(projectId));
        }
    }

    @Test
    void checkAccessible_whenNeitherMemberNorScope_shouldThrowDenied() {
        Long projectId = 1104L;
        Long loginUserId = 2104L;
        when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
                .thenReturn(Collections.emptyList());
        ProjectDO project = new ProjectDO();
        project.setId(projectId);
        project.setDirectionCode("D-NET");
        when(projectMapper.selectById(projectId)).thenReturn(project);
        when(objectDataScopeService.compute(loginUserId, "project"))
                .thenReturn(ObjectDataScope.empty());

        try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId)) {
            ServiceException ex = assertThrows(ServiceException.class,
                    () -> permissionService.checkAccessible(projectId));
            assertEquals(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED.getCode(), ex.getCode());
            assertFalse(ex.getMessage().contains(":"), "message 不应外泄权限码");
        }
    }

    @Test
    void checkAccessible_whenObjectNotExists_shouldThrowDenied() {
        Long projectId = 1105L;
        Long loginUserId = 2105L;
        when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
                .thenReturn(Collections.emptyList());
        when(projectMapper.selectById(projectId)).thenReturn(null);

        try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId)) {
            ServiceException ex = assertThrows(ServiceException.class,
                    () -> permissionService.checkAccessible(projectId));
            // spec §3.3 定稿：对象不存在也返回 DENIED，不暴露存在性
            assertEquals(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED.getCode(), ex.getCode());
        }
        // 对象不存在即终止，不再计算数据范围
        verifyNoInteractions(objectDataScopeService);
    }

    // ===== checkPermissionOrVisibility =====

    @Test
    void checkPermissionOrVisibility_whenMemberHasQuery_shouldPassWithoutVisibility() {
        Long projectId = 1201L, loginUserId = 2201L;
        when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
                .thenReturn(List.of(createMember(projectId, loginUserId, 3201L)));
        when(objectPermissionApi.getObjectRolePermissions(3201L,
                PermissionScopeTypeEnum.OBJECT.getScopeType(), "project"))
                .thenReturn(success(Set.of("project:task:query")));

        try (MockedStatic<SecurityFrameworkUtils> s = mockLoginUser(loginUserId)) {
            assertDoesNotThrow(() -> permissionService.checkPermissionOrVisibility(projectId, "project:task:query"));
        }
        // 成员凭权限码即放行，不触达可见性配置兜底
        verifyNoInteractions(userVisibilityConfigApi);
    }

    @Test
    void checkPermissionOrVisibility_whenMemberNoQueryButVisibilityAll_shouldPass() {
        Long projectId = 1202L, loginUserId = 2202L;
        when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
                .thenReturn(List.of(createMember(projectId, loginUserId, 3202L)));
        when(objectPermissionApi.getObjectRolePermissions(3202L,
                PermissionScopeTypeEnum.OBJECT.getScopeType(), "project"))
                .thenReturn(success(Set.of("project:task:update")));   // 成员有角色但无 query
        when(userVisibilityConfigApi.getConfig(loginUserId)).thenReturn(success(cfgAll()));

        try (MockedStatic<SecurityFrameworkUtils> s = mockLoginUser(loginUserId)) {
            assertDoesNotThrow(() -> permissionService.checkPermissionOrVisibility(projectId, "project:task:query"));
        }
    }

    @Test
    void checkPermissionOrVisibility_whenMemberNoQueryButVisibilityDirectionHit_shouldPass() {
        Long projectId = 1203L, loginUserId = 2203L;
        when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
                .thenReturn(List.of(createMember(projectId, loginUserId, 3203L)));
        when(objectPermissionApi.getObjectRolePermissions(3203L,
                PermissionScopeTypeEnum.OBJECT.getScopeType(), "project"))
                .thenReturn(success(Set.of("project:task:update")));
        when(userVisibilityConfigApi.getConfig(loginUserId)).thenReturn(success(cfgDirections("D-NET")));
        when(projectMapper.selectById(projectId)).thenReturn(projectOf(projectId, "D-NET"));

        try (MockedStatic<SecurityFrameworkUtils> s = mockLoginUser(loginUserId)) {
            assertDoesNotThrow(() -> permissionService.checkPermissionOrVisibility(projectId, "project:task:query"));
        }
    }

    @Test
    void checkPermissionOrVisibility_whenNonMemberButVisibilityAll_shouldPass() {
        Long projectId = 1204L, loginUserId = 2204L;
        when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
                .thenReturn(Collections.emptyList());
        when(userVisibilityConfigApi.getConfig(loginUserId)).thenReturn(success(cfgAll()));

        try (MockedStatic<SecurityFrameworkUtils> s = mockLoginUser(loginUserId)) {
            assertDoesNotThrow(() -> permissionService.checkPermissionOrVisibility(projectId, "project:task:query"));
        }
        // 非成员不查角色权限码
        verifyNoInteractions(objectPermissionApi);
    }

    @Test
    void checkPermissionOrVisibility_whenNonMemberDirectionHit_shouldPass() {
        Long projectId = 1205L, loginUserId = 2205L;
        when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
                .thenReturn(Collections.emptyList());
        when(userVisibilityConfigApi.getConfig(loginUserId)).thenReturn(success(cfgDirections("D-NET")));
        when(projectMapper.selectById(projectId)).thenReturn(projectOf(projectId, "D-NET"));

        try (MockedStatic<SecurityFrameworkUtils> s = mockLoginUser(loginUserId)) {
            assertDoesNotThrow(() -> permissionService.checkPermissionOrVisibility(projectId, "project:task:query"));
        }
    }

    @Test
    void checkPermissionOrVisibility_whenNonMemberNoConfig_shouldThrowDenied() {
        Long projectId = 1206L, loginUserId = 2206L;
        when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
                .thenReturn(Collections.emptyList());
        when(userVisibilityConfigApi.getConfig(loginUserId)).thenReturn(success(null));   // 无配置

        try (MockedStatic<SecurityFrameworkUtils> s = mockLoginUser(loginUserId)) {
            ServiceException ex = assertThrows(ServiceException.class,
                    () -> permissionService.checkPermissionOrVisibility(projectId, "project:task:query"));
            assertEquals(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED.getCode(), ex.getCode());
            assertFalse(ex.getMessage().contains(":"), "message 不应外泄权限码");
        }
    }

    @Test
    void checkPermissionOrVisibility_whenNonMemberDirectionMiss_shouldThrowDenied() {
        Long projectId = 1207L, loginUserId = 2207L;
        when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
                .thenReturn(Collections.emptyList());
        when(userVisibilityConfigApi.getConfig(loginUserId)).thenReturn(success(cfgDirections("D-OTHER")));
        when(projectMapper.selectById(projectId)).thenReturn(projectOf(projectId, "D-NET"));

        try (MockedStatic<SecurityFrameworkUtils> s = mockLoginUser(loginUserId)) {
            ServiceException ex = assertThrows(ServiceException.class,
                    () -> permissionService.checkPermissionOrVisibility(projectId, "project:task:query"));
            assertEquals(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED.getCode(), ex.getCode());
        }
    }

    @Test
    void checkPermissionOrVisibility_whenMemberNoQueryAndNoVisibility_shouldThrowDenied() {
        Long projectId = 1208L, loginUserId = 2208L;
        when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
                .thenReturn(List.of(createMember(projectId, loginUserId, 3208L)));
        when(objectPermissionApi.getObjectRolePermissions(3208L,
                PermissionScopeTypeEnum.OBJECT.getScopeType(), "project"))
                .thenReturn(success(Set.of("project:task:update")));
        when(userVisibilityConfigApi.getConfig(loginUserId)).thenReturn(success(null));

        try (MockedStatic<SecurityFrameworkUtils> s = mockLoginUser(loginUserId)) {
            ServiceException ex = assertThrows(ServiceException.class,
                    () -> permissionService.checkPermissionOrVisibility(projectId, "project:task:query"));
            assertEquals(ErrorCodeConstants.PROJECT_OBJECT_PERMISSION_DENIED.getCode(), ex.getCode());
            assertFalse(ex.getMessage().contains(":"), "message 不应外泄权限码");
        }
    }

    private UserVisibilityConfigRespDTO cfgAll() {
        UserVisibilityConfigRespDTO cfg = new UserVisibilityConfigRespDTO();
        cfg.setType("all");
        return cfg;
    }

    private UserVisibilityConfigRespDTO cfgDirections(String... codes) {
        UserVisibilityConfigRespDTO cfg = new UserVisibilityConfigRespDTO();
        cfg.setType("directions");
        cfg.setDirectionCodes(Set.of(codes));
        return cfg;
    }

    private ProjectDO projectOf(Long projectId, String directionCode) {
        ProjectDO project = new ProjectDO();
        project.setId(projectId);
        project.setDirectionCode(directionCode);
        return project;
    }

    private UserObjectRoleDO createMember(Long projectId, Long loginUserId, Long roleId) {
        UserObjectRoleDO member = new UserObjectRoleDO();
        member.setId(9001L);
        member.setUserId(loginUserId);
        member.setObjectType("project");
        member.setObjectId(projectId);
        member.setRoleId(roleId);
        member.setStatus(0);
        return member;
    }

    private MockedStatic<SecurityFrameworkUtils> mockLoginUser(Long loginUserId) {
        MockedStatic<SecurityFrameworkUtils> mockedStatic = mockStatic(SecurityFrameworkUtils.class);
        mockedStatic.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(loginUserId);
        return mockedStatic;
    }

}
