import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository, IsNull } from 'typeorm';
import { ForbiddenException, NotFoundException } from '@nestjs/common';
import { ChildrenService } from './children.service';
import { Child } from '../../database/entities/child.entity';
import { FamilyMember } from '../../database/entities/family-member.entity';
import { CreateChildDto, Gender } from './dto/create-child.dto';
import { UpdateChildDto } from './dto/update-child.dto';

describe('ChildrenService', () => {
  let service: ChildrenService;
  let childRepository: Repository<Child>;
  let familyMemberRepository: Repository<FamilyMember>;

  const mockUser = {
    id: 'usr_test123',
    familyId: 'fam_test123',
  };

  const mockChild = {
    id: 'chd_test123',
    familyId: 'fam_test123',
    name: 'Emma',
    birthDate: new Date('2023-06-15'),
    gender: Gender.FEMALE,
    photoUrl: null,
    medicalInfo: {},
    createdAt: new Date(),
    deletedAt: null,
  };

  const mockMembership = {
    userId: 'usr_test123',
    familyId: 'fam_test123',
    role: 'parent',
    permissions: {
      canAddChildren: true,
      canEditChildren: true,
      canLogActivities: true,
      canViewReports: true,
    },
  };

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

    service = module.get<ChildrenService>(ChildrenService);
    childRepository = module.get<Repository<Child>>(getRepositoryToken(Child));
    familyMemberRepository = module.get<Repository<FamilyMember>>(
      getRepositoryToken(FamilyMember),
    );
  });

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

  describe('create', () => {
    const createChildDto: CreateChildDto = {
      name: 'Emma',
      birthDate: '2023-06-15',
      gender: Gender.FEMALE,
    };

    it('should successfully create a child', async () => {
      jest
        .spyOn(familyMemberRepository, 'findOne')
        .mockResolvedValue(mockMembership as any);
      jest.spyOn(childRepository, 'create').mockReturnValue(mockChild as any);
      jest.spyOn(childRepository, 'save').mockResolvedValue(mockChild as any);

      const result = await service.create(
        mockUser.id,
        mockUser.familyId,
        createChildDto,
      );

      expect(result).toEqual(mockChild);
      expect(familyMemberRepository.findOne).toHaveBeenCalledWith({
        where: { userId: mockUser.id, familyId: mockUser.familyId },
      });
      expect(childRepository.create).toHaveBeenCalled();
      expect(childRepository.save).toHaveBeenCalled();
    });

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

      await expect(
        service.create(mockUser.id, mockUser.familyId, createChildDto),
      ).rejects.toThrow(ForbiddenException);
    });

    it('should throw ForbiddenException if user lacks canAddChildren permission', async () => {
      const membershipWithoutPermission = {
        ...mockMembership,
        permissions: {
          ...mockMembership.permissions,
          canAddChildren: false,
        },
      };

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

      await expect(
        service.create(mockUser.id, mockUser.familyId, createChildDto),
      ).rejects.toThrow(ForbiddenException);
    });
  });

  describe('findAll', () => {
    it('should return all active children for a family', async () => {
      jest
        .spyOn(familyMemberRepository, 'findOne')
        .mockResolvedValue(mockMembership as any);
      jest.spyOn(childRepository, 'find').mockResolvedValue([mockChild] as any);

      const result = await service.findAll(mockUser.id, mockUser.familyId);

      expect(result).toEqual([mockChild]);
      expect(childRepository.find).toHaveBeenCalledWith({
        where: {
          familyId: mockUser.familyId,
          deletedAt: IsNull(),
        },
        order: {
          birthDate: 'DESC',
        },
      });
    });

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

      await expect(
        service.findAll(mockUser.id, mockUser.familyId),
      ).rejects.toThrow(ForbiddenException);
    });
  });

  describe('findOne', () => {
    it('should return a specific child', async () => {
      const childWithFamily = {
        ...mockChild,
        family: { id: 'fam_test123' },
      };

      jest
        .spyOn(childRepository, 'findOne')
        .mockResolvedValue(childWithFamily as any);
      jest
        .spyOn(familyMemberRepository, 'findOne')
        .mockResolvedValue(mockMembership as any);

      const result = await service.findOne(mockUser.id, mockChild.id);

      expect(result).toEqual(childWithFamily);
    });

    it('should throw NotFoundException if child not found', async () => {
      jest.spyOn(childRepository, 'findOne').mockResolvedValue(null);

      await expect(
        service.findOne(mockUser.id, 'chd_nonexistent'),
      ).rejects.toThrow(NotFoundException);
    });

    it("should throw ForbiddenException if user is not a member of the child's family", async () => {
      jest
        .spyOn(childRepository, 'findOne')
        .mockResolvedValue(mockChild as any);
      jest.spyOn(familyMemberRepository, 'findOne').mockResolvedValue(null);

      await expect(service.findOne(mockUser.id, mockChild.id)).rejects.toThrow(
        ForbiddenException,
      );
    });
  });

  describe('update', () => {
    const updateChildDto: UpdateChildDto = {
      name: 'Emma Updated',
    };

    it('should successfully update a child', async () => {
      const updatedChild = {
        ...mockChild,
        name: 'Emma Updated',
      };

      jest
        .spyOn(childRepository, 'findOne')
        .mockResolvedValue(mockChild as any);
      jest
        .spyOn(familyMemberRepository, 'findOne')
        .mockResolvedValue(mockMembership as any);
      jest
        .spyOn(childRepository, 'save')
        .mockResolvedValue(updatedChild as any);

      const result = await service.update(
        mockUser.id,
        mockChild.id,
        updateChildDto,
      );

      expect(result.name).toBe('Emma Updated');
      expect(childRepository.save).toHaveBeenCalled();
    });

    it('should throw NotFoundException if child not found', async () => {
      jest.spyOn(childRepository, 'findOne').mockResolvedValue(null);

      await expect(
        service.update(mockUser.id, 'chd_nonexistent', updateChildDto),
      ).rejects.toThrow(NotFoundException);
    });

    it('should throw ForbiddenException if user lacks canEditChildren permission', async () => {
      const membershipWithoutPermission = {
        ...mockMembership,
        permissions: {
          ...mockMembership.permissions,
          canEditChildren: false,
        },
      };

      jest
        .spyOn(childRepository, 'findOne')
        .mockResolvedValue(mockChild as any);
      jest
        .spyOn(familyMemberRepository, 'findOne')
        .mockResolvedValue(membershipWithoutPermission as any);

      await expect(
        service.update(mockUser.id, mockChild.id, updateChildDto),
      ).rejects.toThrow(ForbiddenException);
    });
  });

  describe('remove', () => {
    it('should soft delete a child', async () => {
      jest
        .spyOn(childRepository, 'findOne')
        .mockResolvedValue(mockChild as any);
      jest
        .spyOn(familyMemberRepository, 'findOne')
        .mockResolvedValue(mockMembership as any);
      jest.spyOn(childRepository, 'save').mockResolvedValue({
        ...mockChild,
        deletedAt: new Date(),
      } as any);

      await service.remove(mockUser.id, mockChild.id);

      expect(childRepository.save).toHaveBeenCalledWith(
        expect.objectContaining({
          deletedAt: expect.any(Date),
        }),
      );
    });

    it('should throw NotFoundException if child not found', async () => {
      jest.spyOn(childRepository, 'findOne').mockResolvedValue(null);

      await expect(
        service.remove(mockUser.id, 'chd_nonexistent'),
      ).rejects.toThrow(NotFoundException);
    });

    it('should throw ForbiddenException if user lacks canEditChildren permission', async () => {
      const membershipWithoutPermission = {
        ...mockMembership,
        permissions: {
          ...mockMembership.permissions,
          canEditChildren: false,
        },
      };

      jest
        .spyOn(childRepository, 'findOne')
        .mockResolvedValue(mockChild as any);
      jest
        .spyOn(familyMemberRepository, 'findOne')
        .mockResolvedValue(membershipWithoutPermission as any);

      await expect(service.remove(mockUser.id, mockChild.id)).rejects.toThrow(
        ForbiddenException,
      );
    });
  });

  describe('getChildAgeInMonths', () => {
    it('should calculate child age in months', async () => {
      const oneYearAgo = new Date();
      oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);

      const childOneYearOld = {
        ...mockChild,
        birthDate: oneYearAgo,
      };

      jest
        .spyOn(childRepository, 'findOne')
        .mockResolvedValue(childOneYearOld as any);

      const result = await service.getChildAgeInMonths(mockChild.id);

      expect(result).toBe(12);
    });

    it('should throw NotFoundException if child not found', async () => {
      jest.spyOn(childRepository, 'findOne').mockResolvedValue(null);

      await expect(
        service.getChildAgeInMonths('chd_nonexistent'),
      ).rejects.toThrow(NotFoundException);
    });
  });

  describe('findAllForUser', () => {
    it("should return all children across user's families", async () => {
      jest
        .spyOn(familyMemberRepository, 'find')
        .mockResolvedValue([mockMembership] as any);

      const mockQueryBuilder = {
        where: jest.fn().mockReturnThis(),
        andWhere: jest.fn().mockReturnThis(),
        orderBy: jest.fn().mockReturnThis(),
        getMany: jest.fn().mockResolvedValue([mockChild]),
      };

      jest
        .spyOn(childRepository, 'createQueryBuilder')
        .mockReturnValue(mockQueryBuilder as any);

      const result = await service.findAllForUser(mockUser.id);

      expect(result).toEqual([mockChild]);
      expect(familyMemberRepository.find).toHaveBeenCalledWith({
        where: { userId: mockUser.id },
      });
    });

    it('should return empty array if user has no family memberships', async () => {
      jest.spyOn(familyMemberRepository, 'find').mockResolvedValue([]);

      const result = await service.findAllForUser(mockUser.id);

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