import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository, Between, MoreThanOrEqual, LessThanOrEqual } from 'typeorm';
import { ForbiddenException, NotFoundException } from '@nestjs/common';
import { TrackingService } from './tracking.service';
import {
  Activity,
  ActivityType,
} from '../../database/entities/activity.entity';
import { Child } from '../../database/entities/child.entity';
import { FamilyMember } from '../../database/entities/family-member.entity';
import { CreateActivityDto } from './dto/create-activity.dto';
import { UpdateActivityDto } from './dto/update-activity.dto';

describe('TrackingService', () => {
  let service: TrackingService;
  let activityRepository: Repository<Activity>;
  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'),
  };

  const mockActivity = {
    id: 'act_test123',
    childId: 'chd_test123',
    type: ActivityType.FEEDING,
    startedAt: new Date('2025-09-30T12:00:00Z'),
    endedAt: new Date('2025-09-30T12:30:00Z'),
    loggedBy: 'usr_test123',
    notes: 'Ate well',
    metadata: { amount: '120ml', type: 'bottle' },
    child: mockChild,
  };

  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: [
        TrackingService,
        {
          provide: getRepositoryToken(Activity),
          useValue: {
            create: jest.fn(),
            save: jest.fn(),
            find: jest.fn(),
            findOne: jest.fn(),
            remove: jest.fn(),
          },
        },
        {
          provide: getRepositoryToken(Child),
          useValue: {
            findOne: jest.fn(),
          },
        },
        {
          provide: getRepositoryToken(FamilyMember),
          useValue: {
            findOne: jest.fn(),
          },
        },
      ],
    }).compile();

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

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

  describe('create', () => {
    const createActivityDto: CreateActivityDto = {
      type: ActivityType.FEEDING,
      startedAt: '2025-09-30T12:00:00Z',
      endedAt: '2025-09-30T12:30:00Z',
      notes: 'Ate well',
      metadata: { amount: '120ml', type: 'bottle' },
    };

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

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

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

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

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

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

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

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

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

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

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

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

      expect(result).toEqual([mockActivity]);
      expect(activityRepository.find).toHaveBeenCalledWith({
        where: { childId: mockChild.id },
        order: { startedAt: 'DESC' },
        relations: ['child', 'logger'],
      });
    });

    it('should filter by activity type', async () => {
      jest
        .spyOn(childRepository, 'findOne')
        .mockResolvedValue(mockChild as any);
      jest
        .spyOn(familyMemberRepository, 'findOne')
        .mockResolvedValue(mockMembership as any);
      jest
        .spyOn(activityRepository, 'find')
        .mockResolvedValue([mockActivity] as any);

      const result = await service.findAll(
        mockUser.id,
        mockChild.id,
        'feeding',
      );

      expect(result).toEqual([mockActivity]);
      expect(activityRepository.find).toHaveBeenCalledWith({
        where: { childId: mockChild.id, type: 'feeding' },
        order: { startedAt: 'DESC' },
        relations: ['child', 'logger'],
      });
    });

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

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

  describe('findOne', () => {
    it('should return a specific activity', async () => {
      const activityWithChild = {
        ...mockActivity,
        child: mockChild,
      };

      jest
        .spyOn(activityRepository, 'findOne')
        .mockResolvedValue(activityWithChild as any);
      jest
        .spyOn(familyMemberRepository, 'findOne')
        .mockResolvedValue(mockMembership as any);

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

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

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

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

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

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

  describe('update', () => {
    const updateActivityDto: UpdateActivityDto = {
      notes: 'Updated notes',
    };

    it('should successfully update an activity', async () => {
      const updatedActivity = {
        ...mockActivity,
        notes: 'Updated notes',
      };

      jest
        .spyOn(activityRepository, 'findOne')
        .mockResolvedValue(mockActivity as any);
      jest
        .spyOn(familyMemberRepository, 'findOne')
        .mockResolvedValue(mockMembership as any);
      jest
        .spyOn(activityRepository, 'save')
        .mockResolvedValue(updatedActivity as any);

      const result = await service.update(
        mockUser.id,
        mockActivity.id,
        updateActivityDto,
      );

      expect(result.notes).toBe('Updated notes');
      expect(activityRepository.save).toHaveBeenCalled();
    });

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

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

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

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

      await expect(
        service.update(mockUser.id, mockActivity.id, updateActivityDto),
      ).rejects.toThrow(ForbiddenException);
    });
  });

  describe('remove', () => {
    it('should delete an activity', async () => {
      jest
        .spyOn(activityRepository, 'findOne')
        .mockResolvedValue(mockActivity as any);
      jest
        .spyOn(familyMemberRepository, 'findOne')
        .mockResolvedValue(mockMembership as any);
      jest
        .spyOn(activityRepository, 'remove')
        .mockResolvedValue(mockActivity as any);

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

      expect(activityRepository.remove).toHaveBeenCalledWith(mockActivity);
    });

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

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

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

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

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

  describe('getDailySummary', () => {
    it('should return daily summary for a child', async () => {
      jest
        .spyOn(childRepository, 'findOne')
        .mockResolvedValue(mockChild as any);
      jest
        .spyOn(familyMemberRepository, 'findOne')
        .mockResolvedValue(mockMembership as any);
      jest
        .spyOn(activityRepository, 'find')
        .mockResolvedValue([mockActivity] as any);

      const result = await service.getDailySummary(
        mockUser.id,
        mockChild.id,
        '2025-09-30',
      );

      expect(result).toHaveProperty('date');
      expect(result).toHaveProperty('childId');
      expect(result).toHaveProperty('activities');
      expect(result).toHaveProperty('byType');
      expect(result.activities).toHaveLength(1);
      expect(result.byType[ActivityType.FEEDING]).toHaveLength(1);
    });

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

      await expect(
        service.getDailySummary(mockUser.id, 'chd_nonexistent', '2025-09-30'),
      ).rejects.toThrow(NotFoundException);
    });
  });
});
