import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { ConflictException } from '@nestjs/common';
import { Not, Repository } from 'typeorm';
import { ClassroomRentalsService } from './classroom-rentals.service';
import { ClassroomRental } from '../entities/classroom-rental.entity';
import { Classroom } from '../entities/classroom.entity';
import { Organization } from '../entities/organization.entity';
import { ClassSchedule } from '../entities/class-schedule.entity';
import { CreateRentalDto, UpdateRentalDto } from './dto/rental.dto';

function mockQueryBuilder<T>(results: T[] = []) {
  const qb = {
    where: jest.fn().mockReturnThis(),
    andWhere: jest.fn().mockReturnThis(),
    leftJoinAndSelect: jest.fn().mockReturnThis(),
    getMany: jest.fn().mockResolvedValue(results),
  };
  return qb;
}

describe('ClassroomRentalsService — findConflicts', () => {
  let service: ClassroomRentalsService;
  let rentalRepo: jest.Mocked<Pick<Repository<ClassroomRental>, 'createQueryBuilder'>>;
  let scheduleRepo: jest.Mocked<Pick<Repository<ClassSchedule>, 'createQueryBuilder'>>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        ClassroomRentalsService,
        {
          provide: getRepositoryToken(ClassroomRental),
          useValue: { createQueryBuilder: jest.fn() },
        },
        { provide: getRepositoryToken(Classroom), useValue: {} },
        { provide: getRepositoryToken(Organization), useValue: {} },
        { provide: getRepositoryToken(ClassSchedule), useValue: { createQueryBuilder: jest.fn() } },
      ],
    }).compile();

    service = module.get<ClassroomRentalsService>(ClassroomRentalsService);
    rentalRepo = module.get(getRepositoryToken(ClassroomRental));
    scheduleRepo = module.get(getRepositoryToken(ClassSchedule));
  });

  it('returns rental conflicts when no schedule conflicts exist', async () => {
    const rental = {
      id: 1,
      startDate: '2026-03-01',
      endDate: '2026-03-31',
      organization: { name: 'A机构' },
    } as ClassroomRental;
    const rentalQb = mockQueryBuilder<ClassroomRental>([rental]);
    const scheduleQb = mockQueryBuilder<ClassSchedule>([]);
    (rentalRepo.createQueryBuilder as jest.Mock).mockReturnValue(rentalQb);
    (scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(scheduleQb);

    const result = await service.findConflicts(1, '2026-03-15', '2026-04-15');

    expect(result).toHaveLength(1);
    expect(result[0].id).toBe(1);
  });

  it('throws ConflictException when an active schedule overlaps the same classroom and date range', async () => {
    const rentalQb = mockQueryBuilder<ClassroomRental>([]);
    const scheduleQb = mockQueryBuilder<ClassSchedule>([
      {
        id: 5,
        subject: '数学',
        weekDay: 1,
        startDate: '2026-03-01',
        endDate: '2026-06-30',
      } as ClassSchedule,
    ]);
    (rentalRepo.createQueryBuilder as jest.Mock).mockReturnValue(rentalQb);
    (scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(scheduleQb);

    await expect(service.findConflicts(1, '2026-03-15', '2026-04-15')).rejects.toThrow(
      ConflictException,
    );
  });

  it('does not treat a weekly schedule as a conflict when its weekday does not occur in the rental range', async () => {
    const rentalQb = mockQueryBuilder<ClassroomRental>([]);
    const scheduleQb = mockQueryBuilder<ClassSchedule>([
      {
        id: 5,
        subject: '数学',
        weekDay: 1,
        startDate: '2026-07-01',
        endDate: '2026-07-31',
      } as ClassSchedule,
    ]);
    (rentalRepo.createQueryBuilder as jest.Mock).mockReturnValue(rentalQb);
    (scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(scheduleQb);

    const result = await service.findConflicts(1, '2026-07-01', '2026-07-05');

    expect(result).toHaveLength(0);
  });

  it('does not throw when schedule is outside the requested date range', async () => {
    const rentalQb = mockQueryBuilder<ClassroomRental>([]);
    const scheduleQb = mockQueryBuilder<ClassSchedule>([]);
    (rentalRepo.createQueryBuilder as jest.Mock).mockReturnValue(rentalQb);
    (scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(scheduleQb);

    const result = await service.findConflicts(1, '2026-07-01', '2026-08-31');

    expect(result).toHaveLength(0);
  });

  it('excludes the given rental id from rental conflict check', async () => {
    const rentalQb = mockQueryBuilder<ClassroomRental>([]);
    const scheduleQb = mockQueryBuilder<ClassSchedule>([]);
    (rentalRepo.createQueryBuilder as jest.Mock).mockReturnValue(rentalQb);
    (scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(scheduleQb);

    await service.findConflicts(1, '2026-03-01', '2026-03-31', 99);

    expect(rentalQb.andWhere).toHaveBeenCalledWith('r.id != :excludeId', { excludeId: 99 });
  });
});

describe('ClassroomRentalsService — unavailable dates', () => {
  let service: ClassroomRentalsService;
  let rentalRepo: jest.Mocked<Pick<Repository<ClassroomRental>, 'find'>>;
  let scheduleRepo: jest.Mocked<Pick<Repository<ClassSchedule>, 'find'>>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        ClassroomRentalsService,
        { provide: getRepositoryToken(ClassroomRental), useValue: { find: jest.fn() } },
        { provide: getRepositoryToken(Classroom), useValue: {} },
        { provide: getRepositoryToken(Organization), useValue: {} },
        { provide: getRepositoryToken(ClassSchedule), useValue: { find: jest.fn() } },
      ],
    }).compile();

    service = module.get<ClassroomRentalsService>(ClassroomRentalsService);
    rentalRepo = module.get(getRepositoryToken(ClassroomRental));
    scheduleRepo = module.get(getRepositoryToken(ClassSchedule));
  });

  it('returns rental days and actual weekly schedule occurrence dates for a month', async () => {
    rentalRepo.find.mockResolvedValue([
      { id: 10, startDate: '2026-07-03', endDate: '2026-07-04' } as ClassroomRental,
    ]);
    scheduleRepo.find.mockResolvedValue([
      { id: 5, weekDay: 1, startDate: '2026-07-01', endDate: '2026-07-31' } as ClassSchedule,
    ]);

    const result = await service.getUnavailableDates(1, 2026, 7);

    expect(result).toEqual({
      dates: ['2026-07-03', '2026-07-04', '2026-07-06', '2026-07-13', '2026-07-20', '2026-07-27'],
    });
  });

  it('excludes the rental being edited', async () => {
    rentalRepo.find.mockResolvedValue([]);
    scheduleRepo.find.mockResolvedValue([]);

    await service.getUnavailableDates(1, 2026, 7, 99);

    expect(rentalRepo.find).toHaveBeenCalledWith(
      expect.objectContaining({ where: expect.objectContaining({ id: Not(99) }) }),
    );
  });
});

describe('ClassroomRentalsService — rental schedule sync', () => {
  let service: ClassroomRentalsService;
  let rentalRepo: jest.Mocked<
    Pick<
      Repository<ClassroomRental>,
      'findOne' | 'save' | 'create' | 'update' | 'delete' | 'createQueryBuilder'
    >
  >;
  let classroomRepo: jest.Mocked<Pick<Repository<Classroom>, 'findOne'>>;
  let organizationRepo: jest.Mocked<Pick<Repository<Organization>, 'findOne'>>;
  let scheduleRepo: jest.Mocked<
    Pick<
      Repository<ClassSchedule>,
      'findOne' | 'save' | 'create' | 'update' | 'delete' | 'createQueryBuilder'
    >
  >;

  beforeEach(async () => {
    rentalRepo = {
      findOne: jest.fn(),
      save: jest.fn(),
      create: jest.fn(),
      update: jest.fn(),
      delete: jest.fn(),
      createQueryBuilder: jest.fn(),
    } as jest.Mocked<
      Pick<
        Repository<ClassroomRental>,
        'findOne' | 'save' | 'create' | 'update' | 'delete' | 'createQueryBuilder'
      >
    >;

    classroomRepo = { findOne: jest.fn() } as jest.Mocked<Pick<Repository<Classroom>, 'findOne'>>;

    organizationRepo = { findOne: jest.fn() } as jest.Mocked<
      Pick<Repository<Organization>, 'findOne'>
    >;

    scheduleRepo = {
      findOne: jest.fn(),
      save: jest.fn(),
      create: jest.fn(),
      update: jest.fn(),
      delete: jest.fn(),
      createQueryBuilder: jest.fn(),
    } as jest.Mocked<
      Pick<
        Repository<ClassSchedule>,
        'findOne' | 'save' | 'create' | 'update' | 'delete' | 'createQueryBuilder'
      >
    >;

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        ClassroomRentalsService,
        { provide: getRepositoryToken(ClassroomRental), useValue: rentalRepo },
        { provide: getRepositoryToken(Classroom), useValue: classroomRepo },
        { provide: getRepositoryToken(Organization), useValue: organizationRepo },
        { provide: getRepositoryToken(ClassSchedule), useValue: scheduleRepo },
      ],
    }).compile();

    service = module.get<ClassroomRentalsService>(ClassroomRentalsService);
  });

  describe('create()', () => {
    it('saves the rental and creates a RENTAL class_schedule row', async () => {
      const dto: CreateRentalDto = {
        classroomId: 1,
        lesseeOrganizationId: 2,
        startDate: '2026-03-01',
        endDate: '2026-03-31',
      };
      const classroom = { id: 1, status: 'available' } as Classroom;
      const hostOrganization = {
        id: 1,
        name: 'Host',
        isHost: true,
        status: 'active',
      } as Organization;
      const organization = {
        id: 2,
        name: 'Organization A',
        isHost: false,
        status: 'active',
      } as Organization;

      classroomRepo.findOne.mockResolvedValue(classroom);
      organizationRepo.findOne
        .mockResolvedValueOnce(hostOrganization)
        .mockResolvedValueOnce(organization);
      rentalRepo.create.mockImplementation(
        (entity) => ({ ...(entity as object) }) as ClassroomRental,
      );
      rentalRepo.save.mockImplementation((entity) =>
        Promise.resolve({ ...(entity as object), id: 1 } as ClassroomRental),
      );
      rentalRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder<ClassroomRental>([]));
      scheduleRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder<ClassSchedule>([]));
      scheduleRepo.findOne.mockResolvedValue(null);
      scheduleRepo.create.mockImplementation(
        (entity) => ({ ...(entity as object) }) as ClassSchedule,
      );
      scheduleRepo.save.mockImplementation((entity) =>
        Promise.resolve({ ...(entity as object), id: 100 } as ClassSchedule),
      );

      const result = await service.create(dto);

      expect(result.id).toBe(1);
      expect(rentalRepo.save).toHaveBeenCalledWith(
        expect.objectContaining({
          classroomId: 1,
          lessorOrganizationId: 1,
          lesseeOrganizationId: 2,
          startDate: '2026-03-01',
          endDate: '2026-03-31',
          status: 'active',
        }),
      );
      expect(scheduleRepo.create).toHaveBeenCalledWith(
        expect.objectContaining({
          classroomId: 1,
          classId: null,
          startTime: '00:00',
          endTime: '23:59',
          startDate: '2026-03-01',
          endDate: '2026-03-31',
          subject: 'Organization A 租赁',
          teacherId: null,
          scheduleType: 'RENTAL',
          rentalId: 1,
          status: 'active',
        }),
      );
      expect(scheduleRepo.save).toHaveBeenCalled();
    });
  });

  describe('update()', () => {
    it('updates the existing RENTAL schedule row when dates or classroom change', async () => {
      const existingRental = {
        id: 1,
        classroomId: 1,
        lesseeOrganizationId: 2,
        startDate: '2026-07-01',
        endDate: '2099-03-31',
        status: 'active',
        notes: '',
        lesseeOrganization: { id: 2, name: 'Organization A' } as Organization,
        classroom: { id: 1 } as Classroom,
      } as ClassroomRental;
      const updatedRental = {
        ...existingRental,
        startDate: '2026-08-01',
        endDate: '2099-04-30',
      };
      const existingSchedule = {
        id: 50,
        rentalId: 1,
        scheduleType: 'RENTAL',
        classroomId: 1,
      } as ClassSchedule;

      rentalRepo.findOne.mockResolvedValueOnce(existingRental).mockResolvedValueOnce(updatedRental);
      rentalRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder<ClassroomRental>([]));
      scheduleRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder<ClassSchedule>([]));
      scheduleRepo.findOne.mockResolvedValue(existingSchedule);

      const dto: UpdateRentalDto = { startDate: '2026-08-01', endDate: '2099-04-30' };
      await service.update(1, dto);

      expect(rentalRepo.update).toHaveBeenCalledWith(
        1,
        expect.objectContaining({ startDate: '2026-08-01', endDate: '2099-04-30' }),
      );
      expect(scheduleRepo.update).toHaveBeenCalledWith(
        50,
        expect.objectContaining({
          scheduleType: 'RENTAL',
          rentalId: 1,
          classroomId: 1,
          startDate: '2026-08-01',
          endDate: '2099-04-30',
          status: 'active',
          subject: 'Organization A 租赁',
        }),
      );
      expect(scheduleRepo.create).not.toHaveBeenCalled();
      expect(scheduleRepo.delete).not.toHaveBeenCalled();
    });

    it('deactivates the RENTAL schedule row when the rental is cancelled', async () => {
      const rental = {
        id: 1,
        classroomId: 1,
        lesseeOrganizationId: 2,
        startDate: '2026-07-01',
        endDate: '2099-03-31',
        status: 'active',
        lesseeOrganization: { id: 2, name: 'Organization A' } as Organization,
      } as ClassroomRental;
      const cancelledRental = { ...rental, status: 'cancelled' } as ClassroomRental;

      rentalRepo.findOne.mockResolvedValueOnce(rental).mockResolvedValueOnce(cancelledRental);

      await service.cancel(1);

      expect(rentalRepo.update).toHaveBeenCalledWith(1, { status: 'cancelled' });
      expect(scheduleRepo.update).toHaveBeenCalledWith(
        { rentalId: 1, scheduleType: 'RENTAL' },
        { status: 'inactive' },
      );
    });
  });

  describe('lifecycle actions', () => {
    it('ends an active rental and shortens a future end date', async () => {
      const rental = {
        id: 1,
        classroomId: 1,
        startDate: '2026-07-01',
        endDate: '2099-12-31',
        status: 'active',
        lesseeOrganization: { name: 'Organization A' },
      } as ClassroomRental;
      const ended = { ...rental, status: 'ended', endDate: '2026-07-13' } as ClassroomRental;
      rentalRepo.findOne.mockResolvedValueOnce(rental).mockResolvedValueOnce(ended);
      scheduleRepo.findOne.mockResolvedValue({ id: 50 } as ClassSchedule);

      await service.end(1);

      expect(rentalRepo.update).toHaveBeenCalledWith(
        1,
        expect.objectContaining({ status: 'ended' }),
      );
      expect(scheduleRepo.update).toHaveBeenCalled();
    });

    it('rejects ending a future rental', async () => {
      rentalRepo.findOne.mockResolvedValue({
        id: 1,
        startDate: '2099-01-01',
        endDate: '2099-12-31',
        status: 'active',
      } as ClassroomRental);

      await expect(service.end(1)).rejects.toThrow('租赁尚未开始');
    });
  });

  describe('remove()', () => {
    it('archives the rental and deactivates its RENTAL schedule row', async () => {
      const rental = {
        id: 1,
        classroomId: 1,
        lesseeOrganizationId: 2,
        startDate: '2026-03-01',
        endDate: '2026-03-31',
        status: 'active',
        lesseeOrganization: { id: 2, name: 'Organization A' } as Organization,
      } as ClassroomRental;

      rentalRepo.findOne.mockResolvedValue(rental);

      await service.remove(1);

      expect(rentalRepo.update).toHaveBeenCalledWith(1, { status: 'cancelled' });
      expect(scheduleRepo.update).toHaveBeenCalledWith(
        { rentalId: 1, scheduleType: 'RENTAL' },
        { status: 'inactive' },
      );
      expect(rentalRepo.delete).not.toHaveBeenCalled();
    });
  });
});

describe('ClassroomRentalsService — organization roles', () => {
  it('stores explicit lessor and lessee organizations for a rental', async () => {
    const rentalRepo = {
      findOne: jest.fn(),
      save: jest.fn(async (value) => ({ ...value, id: 1 })),
      create: jest.fn((value) => value),
      update: jest.fn(),
      delete: jest.fn(),
      createQueryBuilder: jest.fn().mockReturnValue(mockQueryBuilder<ClassroomRental>([])),
    } as any;
    const classroomRepo = {
      findOne: jest.fn().mockResolvedValue({ id: 1, status: 'available' }),
    } as any;
    const organizationRepo = {
      findOne: jest
        .fn()
        .mockResolvedValueOnce({ id: 1, name: '本机构', isHost: true, status: 'active' })
        .mockResolvedValueOnce({ id: 2, name: '合作机构', isHost: false, status: 'active' }),
    } as any;
    const scheduleRepo = {
      findOne: jest.fn().mockResolvedValue(null),
      save: jest.fn(async (value) => ({ ...value, id: 100 })),
      create: jest.fn((value) => value),
      update: jest.fn(),
      delete: jest.fn(),
      createQueryBuilder: jest.fn().mockReturnValue(mockQueryBuilder<ClassSchedule>([])),
    } as any;

    const service = new ClassroomRentalsService(
      rentalRepo,
      classroomRepo,
      organizationRepo,
      scheduleRepo,
    );

    await service.create({
      classroomId: 1,
      lessorOrganizationId: 1,
      lesseeOrganizationId: 2,
      startDate: '2026-08-01',
      endDate: '2026-08-31',
    });

    expect(rentalRepo.save).toHaveBeenCalledWith(
      expect.objectContaining({
        lessorOrganizationId: 1,
        lesseeOrganizationId: 2,
      }),
    );
  });
});
