/**
 * UserDepartmentsService Unit Tests
 * 
 * 测试用户部门关联服务
 * 
 * 测试覆盖:
 * - 添加用户到部门
 * - 主部门管理
 * - 上级验证（同部门约束）
 * - 移除部门归属
 * - 多部门归属查询
 * 
 * 基于文档: docs/modules/organization/09-test-scenarios.md (Section 3)
 */

import { Test, TestingModule } from '@nestjs/testing';
import { NotFoundException } from '@nestjs/common';
import { UserDepartmentsService } from '@/modules/organization/user-departments/user-departments.service';
import { PrismaService } from '@/core/database/prisma/prisma.service';
import {
  IamUserNotInDepartmentException,
  IamUserAlreadyInDepartmentException,
  IamManagerNotInDepartmentException,
  IamManagerSelfReferenceException,
} from '@/modules/organization/exceptions';

describe('UserDepartmentsService', () => {
  let service: UserDepartmentsService;
  let prisma: PrismaService;

  const mockUserDepartment = {
    id: 'ud-test-001',
    userId: 'user-001',
    departmentId: 'dept-001',
    organizationId: 'org-001',
    positionId: 'pos-001',
    managerId: 'manager-001',
    isPrimary: true,
    title: null,
    joinedAt: new Date(),
    leftAt: null,
    createdAt: new Date(),
    updatedAt: new Date(),
  };

  const mockUser = {
    id: 'user-001',
    username: 'testuser',
    displayName: 'Test User',
    status: 'ACTIVE',
    deletedAt: null,
  };

  const mockDepartment = {
    id: 'dept-001',
    name: 'Test Department',
    code: 'TEST',
    organizationId: 'org-001',
    deletedAt: null,
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UserDepartmentsService,
        {
          provide: PrismaService,
          useValue: {
            userDepartment: {
              create: jest.fn(),
              findUnique: jest.fn(),
              findMany: jest.fn(),
              update: jest.fn(),
              updateMany: jest.fn(),
              delete: jest.fn(),
              count: jest.fn(),
            },
            user: {
              findUnique: jest.fn(),
            },
            department: {
              findUnique: jest.fn(),
            },
            position: {
              findUnique: jest.fn(),
            },
            $transaction: jest.fn(),
          },
        },
      ],
    }).compile();

    service = module.get<UserDepartmentsService>(UserDepartmentsService);
    prisma = module.get<PrismaService>(PrismaService);
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('addUserDepartment', () => {
    it('[测试场景 3.1.1] 应该成功添加用户到部门', async () => {
      const userId = 'user-001';
      const dto = {
        departmentId: 'dept-001',
        positionId: 'pos-001',
        managerId: 'manager-001',
        isPrimary: true,
      };

      const mockPosition = { id: 'pos-001', name: 'Engineer', deletedAt: null };
      const mockManager = {
        ...mockUser,
        id: 'manager-001',
      };

      // Mock manager is in same department
      const mockManagerDept = {
        id: 'ud-manager',
        userId: 'manager-001',
        departmentId: 'dept-001',
        isPrimary: true,
        leftAt: null,
      };

      jest.spyOn(prisma.user, 'findUnique')
        .mockResolvedValueOnce(mockUser as any)  // 验证用户存在
        .mockResolvedValueOnce(mockManager as any);  // 验证上级存在
      jest.spyOn(prisma.userDepartment, 'findUnique')
        .mockResolvedValueOnce(null)  // 用户不在部门中
        .mockResolvedValueOnce(mockManagerDept as any)  // 上级在部门中
        .mockResolvedValueOnce(null);  // 环路检测：manager 没有上级
      jest.spyOn(prisma.department, 'findUnique')
        .mockResolvedValueOnce(mockDepartment as any)  // 部门存在验证
        .mockResolvedValueOnce({ ...mockDepartment } as any);  // 获取 organizationId
      jest.spyOn(prisma.position, 'findUnique').mockResolvedValue(mockPosition as any);
      jest.spyOn(prisma.userDepartment, 'count').mockResolvedValue(0);
      jest.spyOn(prisma.userDepartment, 'updateMany').mockResolvedValue({ count: 0 } as any);
      jest.spyOn(prisma.userDepartment, 'create').mockResolvedValue(mockUserDepartment as any);

      const result = await service.addUserDepartment(userId, dto);

      expect(result).toBeDefined();
      expect(result.userId).toBe(userId);
      expect(result.departmentId).toBe(dto.departmentId);

      expect(prisma.userDepartment.create).toHaveBeenCalled();
    });

    it('[测试场景 3.1.2] 上级必须在同一部门', async () => {
      const userId = 'user-001';
      const dto = {
        departmentId: 'dept-001',
        managerId: 'manager-002',
      };

      jest.spyOn(prisma.user, 'findUnique')
        .mockResolvedValueOnce(mockUser as any)
        .mockResolvedValueOnce({ id: 'manager-002', deletedAt: null } as any);
      jest.spyOn(prisma.userDepartment, 'findUnique')
        .mockResolvedValueOnce(null)  // 用户不在部门
        .mockResolvedValueOnce(null);  // 上级不在同一部门
      jest.spyOn(prisma.department, 'findUnique').mockResolvedValue(mockDepartment as any);

      await expect(service.addUserDepartment(userId, dto)).rejects.toThrow(
        IamManagerNotInDepartmentException
      );

      expect(prisma.userDepartment.create).not.toHaveBeenCalled();
    });

    it('[测试场景 3.1.3] 用户已属于该部门应抛出异常', async () => {
      const userId = 'user-001';
      const dto = {
        departmentId: 'dept-001',
      };

      jest.spyOn(prisma.user, 'findUnique').mockResolvedValue(mockUser as any);
      jest.spyOn(prisma.userDepartment, 'findUnique').mockResolvedValue(mockUserDepartment as any);

      await expect(service.addUserDepartment(userId, dto)).rejects.toThrow(
        IamUserAlreadyInDepartmentException
      );

      expect(prisma.userDepartment.create).not.toHaveBeenCalled();
    });

    it('用户不存在应抛出异常', async () => {
      const userId = 'non-existent';
      const dto = {
        departmentId: 'dept-001',
      };

      jest.spyOn(prisma.user, 'findUnique').mockResolvedValue(null);

      await expect(service.addUserDepartment(userId, dto)).rejects.toThrow(NotFoundException);
    });

    it('部门不存在应抛出异常', async () => {
      const userId = 'user-001';
      const dto = {
        departmentId: 'non-existent',
      };

      jest.spyOn(prisma.user, 'findUnique').mockResolvedValue(mockUser as any);
      jest.spyOn(prisma.userDepartment, 'findUnique').mockResolvedValue(null);
      jest.spyOn(prisma.department, 'findUnique').mockResolvedValue(null);

      await expect(service.addUserDepartment(userId, dto)).rejects.toThrow(NotFoundException);
    });
  });

  describe('setPrimaryDepartment', () => {
    it('[测试场景 3.2.1] 应该成功切换主部门', async () => {
      const userId = 'user-001';
      const newPrimaryDeptId = 'dept-002';

      const membership = {
        ...mockUserDepartment,
        departmentId: newPrimaryDeptId,
        isPrimary: false,
      };

      jest.spyOn(prisma.userDepartment, 'findUnique').mockResolvedValue(membership as any);
      (prisma.$transaction as jest.Mock).mockResolvedValue([
        { count: 1 },  // updateMany result
        { ...membership, isPrimary: true },  // update result
      ]);

      const result = await service.setPrimaryDepartment(userId, newPrimaryDeptId);

      expect(result.userId).toBe(userId);
      expect(result.departmentId).toBe(newPrimaryDeptId);
      expect(prisma.$transaction).toHaveBeenCalled();
    });

    it('用户未在目标部门应抛出异常', async () => {
      const userId = 'user-001';
      const deptId = 'dept-not-member';

      jest.spyOn(prisma.userDepartment, 'findUnique').mockResolvedValue(null);

      await expect(service.setPrimaryDepartment(userId, deptId)).rejects.toThrow(
        IamUserNotInDepartmentException
      );
    });
  });

  describe('removeUserDepartment', () => {
    it('应该成功移除非主部门', async () => {
      const userId = 'user-001';
      const deptId = 'dept-002';

      const nonPrimaryDept = {
        ...mockUserDepartment,
        departmentId: deptId,
        isPrimary: false,
      };

      jest.spyOn(prisma.userDepartment, 'findUnique').mockResolvedValue(nonPrimaryDept as any);
      jest.spyOn(prisma.userDepartment, 'delete').mockResolvedValue(nonPrimaryDept as any);

      const result = await service.removeUserDepartment(userId, deptId);

      expect(result.message).toBeDefined();
      expect(prisma.userDepartment.delete).toHaveBeenCalled();
    });

    it('[测试场景 3.2.2] 应该允许移除主部门（自动切换）', async () => {
      const userId = 'user-001';
      const primaryDeptId = 'dept-001';

      const primaryDept = {
        ...mockUserDepartment,
        departmentId: primaryDeptId,
        isPrimary: true,
      };

      const otherDept = {
        ...mockUserDepartment,
        id: 'ud-other',
        departmentId: 'dept-002',
        isPrimary: false,
      };

      jest.spyOn(prisma.userDepartment, 'findUnique').mockResolvedValue(primaryDept as any);
      jest.spyOn(prisma.userDepartment, 'findMany').mockResolvedValue([otherDept] as any);
      jest.spyOn(prisma.userDepartment, 'update').mockResolvedValue({
        ...otherDept,
        isPrimary: true,
      } as any);
      jest.spyOn(prisma.userDepartment, 'delete').mockResolvedValue(primaryDept as any);

      const result = await service.removeUserDepartment(userId, primaryDeptId);

      expect(result.warning).toBeDefined();
      expect(prisma.userDepartment.delete).toHaveBeenCalled();
    });

    it('用户未在部门中应抛出异常', async () => {
      const userId = 'user-001';
      const deptId = 'dept-not-member';

      jest.spyOn(prisma.userDepartment, 'findUnique').mockResolvedValue(null);

      await expect(service.removeUserDepartment(userId, deptId)).rejects.toThrow(
        IamUserNotInDepartmentException
      );
    });
  });

  describe('getUserDepartments', () => {
    it('应该返回用户的所有部门归属', async () => {
      const userId = 'user-001';

      const primaryDept = {
        ...mockUserDepartment,
        id: 'ud-primary',
        departmentId: 'dept-001',
        isPrimary: true,
        department: { ...mockDepartment, id: 'dept-001', name: 'Primary Dept' },
      };

      const secondaryDept = {
        ...mockUserDepartment,
        id: 'ud-secondary',
        departmentId: 'dept-002',
        isPrimary: false,
        department: { ...mockDepartment, id: 'dept-002', name: 'Secondary Dept' },
      };

      jest.spyOn(prisma.user, 'findUnique').mockResolvedValue(mockUser as any);
      jest.spyOn(prisma.userDepartment, 'findMany').mockResolvedValue([
        primaryDept,
        secondaryDept,
      ] as any);

      const result = await service.getUserDepartments(userId);

      expect(result).toHaveLength(2);
      expect(result[0].isPrimary).toBe(true);
    });

    it('用户不存在应抛出异常', async () => {
      const userId = 'non-existent';

      jest.spyOn(prisma.user, 'findUnique').mockResolvedValue(null);

      await expect(service.getUserDepartments(userId)).rejects.toThrow(NotFoundException);
    });

    it('没有部门归属应返回空列表', async () => {
      const userId = 'user-no-dept';

      jest.spyOn(prisma.user, 'findUnique').mockResolvedValue(mockUser as any);
      jest.spyOn(prisma.userDepartment, 'findMany').mockResolvedValue([]);

      const result = await service.getUserDepartments(userId);

      expect(result).toEqual([]);
    });
  });

  describe('updateUserDepartment', () => {
    it('应该成功更新部门归属信息', async () => {
      const userId = 'user-001';
      const deptId = 'dept-001';
      const updateDto = {
        title: 'Senior Engineer',
        positionId: 'pos-002',
      };

      const updatedDept = {
        ...mockUserDepartment,
        ...updateDto,
        department: mockDepartment,
        position: { id: 'pos-002', name: 'Senior', level: 2 },
        manager: null,
      };

      jest.spyOn(prisma.userDepartment, 'findUnique').mockResolvedValue(mockUserDepartment as any);
      jest.spyOn(prisma.position, 'findUnique').mockResolvedValue({
        id: 'pos-002',
        name: 'Senior',
        deletedAt: null,
      } as any);
      jest.spyOn(prisma.userDepartment, 'update').mockResolvedValue(updatedDept as any);

      const result = await service.updateUserDepartment(userId, deptId, updateDto);

      expect(result.title).toBe('Senior Engineer');
      expect(result.positionId).toBe('pos-002');
    });

    it('应该允许更新上级', async () => {
      const userId = 'user-001';
      const deptId = 'dept-001';
      const newManagerId = 'manager-002';

      const mockNewManagerDept = {
        id: 'ud-new-manager',
        userId: newManagerId,
        departmentId: deptId,
        isPrimary: true,
        leftAt: null,
      };

      jest.spyOn(prisma.userDepartment, 'findUnique')
        .mockResolvedValueOnce(mockUserDepartment as any)  // 当前用户在部门中
        .mockResolvedValueOnce(mockNewManagerDept as any)  // 新上级在同部门
        .mockResolvedValueOnce(null);  // 环路检测：新上级没有上级
      jest.spyOn(prisma.user, 'findUnique').mockResolvedValue({
        id: newManagerId,
        deletedAt: null,
      } as any);
      jest.spyOn(prisma.userDepartment, 'update').mockResolvedValue({
        ...mockUserDepartment,
        managerId: newManagerId,
        department: mockDepartment,
        position: null,
        manager: { id: newManagerId, displayName: 'New Manager', email: 'manager@test.com' },
      } as any);

      const result = await service.updateUserDepartment(userId, deptId, { managerId: newManagerId });

      expect(result.managerId).toBe(newManagerId);
    });

    it('新上级不在同一部门应抛出异常', async () => {
      const userId = 'user-001';
      const deptId = 'dept-001';
      const newManagerId = 'manager-other-dept';

      jest.spyOn(prisma.userDepartment, 'findUnique')
        .mockResolvedValueOnce(mockUserDepartment as any)
        .mockResolvedValueOnce(null);  // 上级不在同部门
      jest.spyOn(prisma.user, 'findUnique').mockResolvedValue({
        id: newManagerId,
        deletedAt: null,
      } as any);

      await expect(
        service.updateUserDepartment(userId, deptId, { managerId: newManagerId })
      ).rejects.toThrow(IamManagerNotInDepartmentException);
    });

    it('用户不在部门中应抛出异常', async () => {
      const userId = 'user-001';
      const deptId = 'dept-not-member';

      jest.spyOn(prisma.userDepartment, 'findUnique').mockResolvedValue(null);

      await expect(
        service.updateUserDepartment(userId, deptId, { title: 'New Title' })
      ).rejects.toThrow(IamUserNotInDepartmentException);
    });
  });

  describe('业务规则验证', () => {
    it('不应该将自己设为自己的上级', async () => {
      const userId = 'user-001';
      const deptId = 'dept-001';

      const dto = {
        departmentId: deptId,
        managerId: userId,  // 自己作为上级
      };

      jest.spyOn(prisma.user, 'findUnique')
        .mockResolvedValueOnce(mockUser as any)  // 用户存在
        .mockResolvedValueOnce(mockUser as any);  // 上级（自己）存在
      jest.spyOn(prisma.userDepartment, 'findUnique').mockResolvedValue(null);
      jest.spyOn(prisma.department, 'findUnique').mockResolvedValue(mockDepartment as any);

      await expect(service.addUserDepartment(userId, dto)).rejects.toThrow(
        IamManagerSelfReferenceException
      );
    });

    it('应该验证上级的组织一致性', async () => {
      const userId = 'user-001';
      const dto = {
        departmentId: 'dept-from-org-a',
        managerId: 'manager-from-org-b',
      };

      const deptOrgA = {
        ...mockDepartment,
        id: 'dept-from-org-a',
        organizationId: 'org-a',
        deletedAt: null,
      };

      jest.spyOn(prisma.user, 'findUnique')
        .mockResolvedValueOnce(mockUser as any)
        .mockResolvedValueOnce({ id: 'manager-from-org-b', deletedAt: null } as any);
      jest.spyOn(prisma.userDepartment, 'findUnique')
        .mockResolvedValueOnce(null)  // 用户不在部门
        .mockResolvedValueOnce(null);  // 上级不在同一部门（因为组织不同）
      jest.spyOn(prisma.department, 'findUnique').mockResolvedValue(deptOrgA as any);

      await expect(service.addUserDepartment(userId, dto)).rejects.toThrow(
        IamManagerNotInDepartmentException
      );
    });
  });
});
