import { Test, TestingModule } from '@nestjs/testing';
import {
  NotFoundException,
  ForbiddenException,
  BadRequestException,
  ConflictException,
} from '@nestjs/common';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { FamiliesService } from './families.service';
import { Family } from '../../database/entities/family.entity';
import {
  FamilyMember,
  FamilyRole,
} from '../../database/entities/family-member.entity';
import { User } from '../../database/entities/user.entity';

describe('FamiliesService', () => {
  let service: FamiliesService;
  let familyRepository: Repository<Family>;
  let familyMemberRepository: Repository<FamilyMember>;
  let userRepository: Repository<User>;

  const mockUser = {
    id: 'usr_123',
    email: 'test@example.com',
    name: 'Test User',
  };

  const mockFamily = {
    id: 'fam_123',
    name: 'Test Family',
    shareCode: 'ABC123',
    members: [],
    children: [],
  };

  const mockMembership = {
    id: 'mem_123',
    userId: 'usr_123',
    familyId: 'fam_123',
    role: FamilyRole.PARENT,
    permissions: {
      canAddChildren: true,
      canEditChildren: true,
      canLogActivities: true,
      canViewReports: true,
      canInviteMembers: true,
    },
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        FamiliesService,
        {
          provide: getRepositoryToken(Family),
          useValue: {
            findOne: jest.fn(),
            save: jest.fn(),
            create: jest.fn(),
          },
        },
        {
          provide: getRepositoryToken(FamilyMember),
          useValue: {
            findOne: jest.fn(),
            save: jest.fn(),
            create: jest.fn(),
            find: jest.fn(),
          },
        },
        {
          provide: getRepositoryToken(User),
          useValue: {
            findOne: jest.fn(),
          },
        },
      ],
    }).compile();

    service = module.get<FamiliesService>(FamiliesService);
    familyRepository = module.get<Repository<Family>>(
      getRepositoryToken(Family),
    );
    familyMemberRepository = module.get<Repository<FamilyMember>>(
      getRepositoryToken(FamilyMember),
    );
    userRepository = module.get<Repository<User>>(getRepositoryToken(User));
  });

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

  describe('inviteMember', () => {
    const inviteDto = {
      email: 'newmember@example.com',
      role: FamilyRole.VIEWER,
    };

    it('should successfully invite a member', async () => {
      jest
        .spyOn(familyMemberRepository, 'findOne')
        .mockResolvedValue(mockMembership as any);
      jest
        .spyOn(familyRepository, 'findOne')
        .mockResolvedValue({ ...mockFamily, members: [] } as any);
      jest.spyOn(userRepository, 'findOne').mockResolvedValue(null);

      const result = await service.inviteMember(
        mockUser.id,
        mockFamily.id,
        inviteDto,
      );

      expect(result).toHaveProperty('shareCode');
      expect(result).toHaveProperty('expiresAt');
      expect(result.shareCode).toBe(mockFamily.shareCode);
    });

    it('should throw ForbiddenException if user is not a member', async () => {
      jest.spyOn(familyMemberRepository, 'findOne').mockResolvedValue(null);

      await expect(
        service.inviteMember(mockUser.id, mockFamily.id, inviteDto),
      ).rejects.toThrow(ForbiddenException);
    });

    it('should throw ForbiddenException if user lacks invite permissions', async () => {
      const memberWithoutPermission = {
        ...mockMembership,
        permissions: { ...mockMembership.permissions, canInviteMembers: false },
      };

      jest
        .spyOn(familyMemberRepository, 'findOne')
        .mockResolvedValue(memberWithoutPermission as any);

      await expect(
        service.inviteMember(mockUser.id, mockFamily.id, inviteDto),
      ).rejects.toThrow(ForbiddenException);
    });

    it('should throw NotFoundException if family not found', async () => {
      jest
        .spyOn(familyMemberRepository, 'findOne')
        .mockResolvedValue(mockMembership as any);
      jest.spyOn(familyRepository, 'findOne').mockResolvedValue(null);

      await expect(
        service.inviteMember(mockUser.id, mockFamily.id, inviteDto),
      ).rejects.toThrow(NotFoundException);
    });

    it('should throw BadRequestException if family has max members', async () => {
      const fullFamily = {
        ...mockFamily,
        members: Array(10).fill(mockMembership),
      };

      jest
        .spyOn(familyMemberRepository, 'findOne')
        .mockResolvedValue(mockMembership as any);
      jest
        .spyOn(familyRepository, 'findOne')
        .mockResolvedValue(fullFamily as any);

      await expect(
        service.inviteMember(mockUser.id, mockFamily.id, inviteDto),
      ).rejects.toThrow(BadRequestException);
    });

    it('should throw ConflictException if user is already a member', async () => {
      const existingUser = { ...mockUser, email: inviteDto.email };

      jest
        .spyOn(familyMemberRepository, 'findOne')
        .mockResolvedValueOnce(mockMembership as any)
        .mockResolvedValueOnce(mockMembership as any);
      jest
        .spyOn(familyRepository, 'findOne')
        .mockResolvedValue({ ...mockFamily, members: [] } as any);
      jest
        .spyOn(userRepository, 'findOne')
        .mockResolvedValue(existingUser as any);

      await expect(
        service.inviteMember(mockUser.id, mockFamily.id, inviteDto),
      ).rejects.toThrow(ConflictException);
    });
  });

  describe('joinFamily', () => {
    const joinDto = {
      shareCode: 'ABC123',
    };

    it('should successfully join a family', async () => {
      const newMember = {
        ...mockMembership,
        role: FamilyRole.VIEWER,
        permissions: {
          canAddChildren: false,
          canEditChildren: false,
          canLogActivities: true,
          canViewReports: true,
          canInviteMembers: false,
        },
      };

      jest
        .spyOn(familyRepository, 'findOne')
        .mockResolvedValue({ ...mockFamily, members: [] } as any);
      jest.spyOn(familyMemberRepository, 'findOne').mockResolvedValue(null);
      jest
        .spyOn(familyMemberRepository, 'create')
        .mockReturnValue(newMember as any);
      jest
        .spyOn(familyMemberRepository, 'save')
        .mockResolvedValue(newMember as any);

      const result = await service.joinFamily(mockUser.id, joinDto);

      expect(result.role).toBe(FamilyRole.VIEWER);
      expect(result.permissions.canLogActivities).toBe(true);
      expect(result.permissions.canInviteMembers).toBe(false);
    });

    it('should throw NotFoundException for invalid share code', async () => {
      jest.spyOn(familyRepository, 'findOne').mockResolvedValue(null);

      await expect(service.joinFamily(mockUser.id, joinDto)).rejects.toThrow(
        NotFoundException,
      );
    });

    it('should throw ConflictException if already a member', async () => {
      jest
        .spyOn(familyRepository, 'findOne')
        .mockResolvedValue({ ...mockFamily, members: [] } as any);
      jest
        .spyOn(familyMemberRepository, 'findOne')
        .mockResolvedValue(mockMembership as any);

      await expect(service.joinFamily(mockUser.id, joinDto)).rejects.toThrow(
        ConflictException,
      );
    });

    it('should throw BadRequestException if family is full', async () => {
      const fullFamily = {
        ...mockFamily,
        members: Array(10).fill(mockMembership),
      };

      jest
        .spyOn(familyRepository, 'findOne')
        .mockResolvedValue(fullFamily as any);
      jest.spyOn(familyMemberRepository, 'findOne').mockResolvedValue(null);

      await expect(service.joinFamily(mockUser.id, joinDto)).rejects.toThrow(
        BadRequestException,
      );
    });
  });

  describe('getFamily', () => {
    it('should return family if user is a member', async () => {
      jest
        .spyOn(familyMemberRepository, 'findOne')
        .mockResolvedValue(mockMembership as any);
      jest
        .spyOn(familyRepository, 'findOne')
        .mockResolvedValue(mockFamily as any);

      const result = await service.getFamily(mockUser.id, mockFamily.id);

      expect(result).toEqual(mockFamily);
      expect(familyRepository.findOne).toHaveBeenCalledWith({
        where: { id: mockFamily.id },
        relations: ['members', 'members.user', 'children'],
      });
    });

    it('should throw ForbiddenException if user is not a member', async () => {
      jest.spyOn(familyMemberRepository, 'findOne').mockResolvedValue(null);

      await expect(
        service.getFamily(mockUser.id, mockFamily.id),
      ).rejects.toThrow(ForbiddenException);
    });

    it('should throw NotFoundException if family not found', async () => {
      jest
        .spyOn(familyMemberRepository, 'findOne')
        .mockResolvedValue(mockMembership as any);
      jest.spyOn(familyRepository, 'findOne').mockResolvedValue(null);

      await expect(
        service.getFamily(mockUser.id, mockFamily.id),
      ).rejects.toThrow(NotFoundException);
    });
  });
});
