import { Repository, DataSource } from 'typeorm';
import { OccupanciesService } from './occupancies.service';
import { Occupancy } from '../entities/occupancy.entity';
import { Room } from '../entities/room.entity';
import { Student } from '../entities/student.entity';
import { Deposit } from '../entities/deposit.entity';
import { Bed } from '../entities/bed.entity';
import { Locker } from '../entities/locker.entity';
import { Organization } from '../entities/organization.entity';

type QueryBuilderMock<T> = {
  where: jest.Mock;
  andWhere: jest.Mock;
  setLock: jest.Mock;
  getOne: jest.Mock<Promise<T | null>, []>;
};

function createQueryBuilderMock<T>(result: T | null): QueryBuilderMock<T> {
  const qb = {
    where: jest.fn(),
    andWhere: jest.fn(),
    setLock: jest.fn(),
    getOne: jest.fn().mockResolvedValue(result),
  } as QueryBuilderMock<T>;
  qb.where.mockReturnValue(qb);
  qb.andWhere.mockReturnValue(qb);
  qb.setLock.mockReturnValue(qb);
  return qb;
}

function createTransactionDataSource(manager: Record<string, unknown>): DataSource {
  return {
    options: { type: 'sqlite' },
    transaction: jest.fn(async (fn: (manager: Record<string, unknown>) => unknown) => fn(manager)),
  } as any as DataSource;
}

function createCheckInManager(options?: {
  existingOccupancy?: Occupancy | null;
  room?: Partial<Room> | null;
  student?: Partial<Student> | null;
  bed?: Partial<Bed> | null;
  locker?: Partial<Locker> | null;
  occupancyCount?: number;
  deposit?: Deposit | null;
}) {
  const manager = {
    createQueryBuilder: jest.fn(),
    count: jest.fn().mockResolvedValue(options?.occupancyCount ?? 0),
    findOne: jest.fn(),
    create: jest.fn((_: unknown, value: unknown) => value),
    save: jest.fn(async (value: any) => ({ ...value, id: value?.id ?? 10 })),
    update: jest.fn(),
  };
  const queryResults = [
    options?.existingOccupancy ?? null,
    options?.room ?? { id: 2, capacity: 4, status: 'available' },
    ...(options?.bed !== undefined ? [options.bed] : []),
    ...(options?.locker !== undefined ? [options.locker] : []),
  ];
  manager.createQueryBuilder.mockImplementation(() =>
    createQueryBuilderMock(queryResults.shift() ?? null),
  );
  manager.findOne.mockImplementation(async (entity: unknown) => {
    if (entity === Student) return options?.student ?? { id: 3, organizationId: 7 };
    if (entity === Deposit) return options?.deposit ?? null;
    return null;
  });
  return manager;
}


function createImportTransactionDataSource(repos: {
  occupancyRepo: Repository<Occupancy>;
  roomRepo: Repository<Room>;
  studentRepo: Repository<Student>;
  depositRepo: Repository<Deposit>;
  bedRepo: Repository<Bed>;
  lockerRepo: Repository<Locker>;
  organizationRepo: Repository<any>;
}): DataSource {
  return createTransactionDataSource({
    getRepository: jest.fn((entity: unknown) => {
      if (entity === Occupancy) return repos.occupancyRepo;
      if (entity === Room) return repos.roomRepo;
      if (entity === Student) return repos.studentRepo;
      if (entity === Deposit) return repos.depositRepo;
      if (entity === Bed) return repos.bedRepo;
      if (entity === Locker) return repos.lockerRepo;
      if (entity === Organization) return repos.organizationRepo;
      throw new Error('Unexpected repository');
    }),
  });
}

function createCheckOutManager(occupancy: Occupancy | null) {
  const manager = {
    createQueryBuilder: jest.fn().mockReturnValue(createQueryBuilderMock(occupancy)),
    save: jest.fn(async (value) => value),
    update: jest.fn(),
  };
  return manager;
}

describe('OccupanciesService — responsible organization', () => {
  it('always takes the responsible organization from the student', async () => {
    const manager = createCheckInManager({
      student: { id: 3, gender: '男', organizationId: 7 },
      bed: { id: 4, roomId: 2, status: 'available' },
    });
    const service = new OccupanciesService(
      {} as Repository<Occupancy>,
      {} as Repository<Room>,
      {} as Repository<Student>,
      {} as Repository<Deposit>,
      {} as Repository<Bed>,
      {} as Repository<Locker>,
      {} as Repository<any>,
      createTransactionDataSource(manager),
    );

    await service.checkIn({
      studentId: 3,
      roomId: 2,
      checkInDate: '2026-07-10',
      bedId: 4,
      responsibleOrganizationId: 99,
    } as any);

    expect(manager.create).toHaveBeenCalledWith(
      Occupancy,
      expect.objectContaining({ responsibleOrganizationId: 7 }),
    );
  });
});

describe('OccupanciesService — manual check-in deposit', () => {
  const createService = (existingDeposit: Deposit | null = null) => {
    const manager = createCheckInManager({
      bed: { id: 4, roomId: 2, status: 'available' },
      deposit: existingDeposit,
    });

    return {
      service: new OccupanciesService(
        {} as Repository<Occupancy>,
        {} as Repository<Room>,
        {} as Repository<Student>,
        {} as Repository<Deposit>,
        {} as Repository<Bed>,
        {} as Repository<Locker>,
        {} as Repository<any>,
        createTransactionDataSource(manager),
      ),
      manager,
    };
  };

  it('creates a paid deposit together with manual check-in', async () => {
    const { service, manager } = createService();

    await service.checkIn(
      {
        studentId: 3,
        roomId: 2,
        checkInDate: '2026-07-14',
        bedId: 4,
        collectDeposit: true,
        depositAmount: 800,
      },
      11,
    );

    expect(manager.create).toHaveBeenCalledWith(Deposit, {
      studentId: 3,
      amount: 800,
      paidDate: '2026-07-14',
      status: 'paid',
      recordedBy: 11,
      notes: '入住登记自动收取',
    });
    expect(manager.save).toHaveBeenCalledWith(expect.objectContaining({ status: 'paid' }));
  });

  it('adds the collected amount to the existing student deposit', async () => {
    const existing = { id: 99, amount: 200, status: 'refunded' } as Deposit;
    const { service, manager } = createService(existing);

    await service.checkIn({
      studentId: 3,
      roomId: 2,
      checkInDate: '2026-07-14',
      bedId: 4,
      collectDeposit: true,
      depositAmount: 800,
    });

    expect(manager.create).toHaveBeenCalledTimes(1);
    expect(manager.save).toHaveBeenCalledWith(
      expect.objectContaining({
        id: 99,
        amount: 1000,
        status: 'paid',
        paidDate: '2026-07-14',
        notes: '入住登记自动收取',
      }),
    );
  });
});

describe('OccupanciesService — import bed capacity', () => {
  it('rejects creating a new bed when the room already has its capacity in beds', async () => {
    const occupancyRepo = {
      findOne: jest.fn().mockResolvedValue(null),
      count: jest.fn().mockResolvedValue(0),
      create: jest.fn((value) => value),
      save: jest.fn(),
    } as any as Repository<Occupancy>;
    const roomRepo = {
      findOne: jest.fn().mockResolvedValue({ id: 2, roomNumber: '4-102', capacity: 4 }),
      create: jest.fn((value) => value),
      save: jest.fn(),
      update: jest.fn(),
    } as any as Repository<Room>;
    const studentRepo = {
      findOne: jest.fn().mockResolvedValue({ id: 3, name: '张三', organizationId: 7 }),
      create: jest.fn((value) => value),
      save: jest.fn(),
      update: jest.fn(),
    } as any as Repository<Student>;
    const bedRepo = {
      findOne: jest.fn().mockResolvedValue(null),
      count: jest.fn().mockResolvedValue(4),
      create: jest.fn((value) => value),
      save: jest.fn(),
      update: jest.fn(),
    } as any as Repository<Bed>;
    const organizationRepo = {
      findOne: jest.fn().mockResolvedValue({ id: 7, name: '本机构', isHost: true }),
      create: jest.fn((value) => value),
      save: jest.fn(),
    } as any as Repository<any>;

    const service = new OccupanciesService(
      occupancyRepo,
      roomRepo,
      studentRepo,
      { findOne: jest.fn(), create: jest.fn(), save: jest.fn() } as any as Repository<Deposit>,
      bedRepo,
      { findOne: jest.fn() } as any as Repository<Locker>,
      organizationRepo,
      createImportTransactionDataSource({
        occupancyRepo,
        roomRepo,
        studentRepo,
        depositRepo: { findOne: jest.fn(), create: jest.fn(), save: jest.fn() } as any as Repository<Deposit>,
        bedRepo,
        lockerRepo: { findOne: jest.fn() } as any as Repository<Locker>,
        organizationRepo,
      }),
    );

    const result = await service.batchImportCheckIn([
      {
        name: '张三',
        phone: '13800138000',
        roomNumber: '4-102',
        bedNumber: '5号床',
        checkInDate: '2026-07-14',
      },
    ]);

    expect(result).toEqual(
      expect.objectContaining({
        imported: 0,
        skipped: 1,
        errors: [expect.stringContaining('不能超过额定人数 4')],
      }),
    );
    expect(bedRepo.save).not.toHaveBeenCalled();
    expect(occupancyRepo.save).not.toHaveBeenCalled();
  });
});

describe('OccupanciesService — import student matching', () => {
  it('associates an existing student by phone and keeps the student organization', async () => {
    const existingStudent = {
      id: 3,
      name: '学生档案姓名',
      phone: '13800138000',
      organizationId: 7,
    };
    const occupancyRepo = {
      findOne: jest.fn().mockResolvedValue(null),
      count: jest.fn().mockResolvedValue(0),
      create: jest.fn((value) => value),
      save: jest.fn(async (value) => ({ ...value, id: 10 })),
    } as any as Repository<Occupancy>;
    const roomRepo = {
      findOne: jest.fn().mockResolvedValue({ id: 2, roomNumber: '4-102', capacity: 4 }),
      create: jest.fn((value) => value),
      save: jest.fn(),
      update: jest.fn(),
    } as any as Repository<Room>;
    const studentRepo = {
      findOne: jest.fn().mockResolvedValue(existingStudent),
      create: jest.fn((value) => value),
      save: jest.fn(),
      update: jest.fn(),
    } as any as Repository<Student>;
    const bedRepo = {
      findOne: jest.fn().mockResolvedValue({
        id: 4,
        roomId: 2,
        bedNumber: '1号床',
        status: 'available',
      }),
      count: jest.fn(),
      create: jest.fn((value) => value),
      save: jest.fn(),
      update: jest.fn(),
    } as any as Repository<Bed>;
    const organizationRepo = {
      findOne: jest.fn(),
      create: jest.fn((value) => value),
      save: jest.fn(),
    } as any as Repository<any>;

    const service = new OccupanciesService(
      occupancyRepo,
      roomRepo,
      studentRepo,
      { findOne: jest.fn() } as any as Repository<Deposit>,
      bedRepo,
      { findOne: jest.fn() } as any as Repository<Locker>,
      organizationRepo,
      createImportTransactionDataSource({
        occupancyRepo,
        roomRepo,
        studentRepo,
        depositRepo: { findOne: jest.fn() } as any as Repository<Deposit>,
        bedRepo,
        lockerRepo: { findOne: jest.fn() } as any as Repository<Locker>,
        organizationRepo,
      }),
    );

    const result = await service.batchImportCheckIn([
      {
        name: 'Excel姓名',
        phone: '13800138000',
        roomNumber: '4-102',
        bedNumber: '1号床',
        checkInDate: '2026-07-14',
      },
    ]);

    expect(studentRepo.findOne).toHaveBeenCalledWith({ where: { phone: '13800138000' } });
    expect(studentRepo.save).not.toHaveBeenCalled();
    expect(organizationRepo.findOne).not.toHaveBeenCalled();
    expect(occupancyRepo.create).toHaveBeenCalledWith(
      expect.objectContaining({ studentId: 3, responsibleOrganizationId: 7 }),
    );
    expect(result).toEqual(expect.objectContaining({ imported: 1, skipped: 0 }));
  });
});

describe('OccupanciesService — stay lifecycle boundaries', () => {
  it('rejects check-out before check-in without releasing resources', async () => {
    const occupancy = {
      id: 1,
      roomId: 2,
      bedId: 3,
      lockerId: 4,
      checkInDate: '2026-07-10',
      billingStartDate: '2026-07-10',
      checkOutDate: null,
    } as Occupancy;
    const manager = createCheckOutManager(occupancy);
    const service = new OccupanciesService(
      {} as Repository<Occupancy>,
      {} as Repository<Room>,
      {} as Repository<Student>,
      {} as Repository<Deposit>,
      {} as Repository<Bed>,
      {} as Repository<Locker>,
      {} as Repository<any>,
      createTransactionDataSource(manager),
    );

    await expect(service.checkOut(1, { checkOutDate: '2026-07-09' })).rejects.toThrow(
      '退宿日期不能早于入住日期',
    );
    expect(manager.save).not.toHaveBeenCalled();
    expect(manager.update).not.toHaveBeenCalled();
  });

  it('rejects check-in to a maintenance room', async () => {
    const manager = createCheckInManager({
      room: { id: 2, capacity: 4, status: 'maintenance' },
      bed: { id: 3, roomId: 2, status: 'available' },
    });
    const service = new OccupanciesService(
      {} as Repository<Occupancy>,
      {} as Repository<Room>,
      {} as Repository<Student>,
      {} as Repository<Deposit>,
      {} as Repository<Bed>,
      {} as Repository<Locker>,
      {} as Repository<any>,
      createTransactionDataSource(manager),
    );

    await expect(
      service.checkIn({ studentId: 1, roomId: 2, checkInDate: '2026-07-10', bedId: 3 }),
    ).rejects.toThrow('该宿舍当前不可入住');
    expect(manager.count).not.toHaveBeenCalled();
  });
});

describe('OccupanciesService — import deposit boundaries', () => {
  const createImportService = (existingDeposit: Deposit | null = null) => {
    const occupancyRepo = {
      findOne: jest.fn().mockResolvedValue(null),
      count: jest.fn().mockResolvedValue(0),
      create: jest.fn((value) => value),
      save: jest.fn(async (value) => ({ ...value, id: 10 })),
    } as any as Repository<Occupancy>;
    const roomRepo = {
      findOne: jest.fn().mockResolvedValue({ id: 2, roomNumber: '4-102', capacity: 4 }),
      update: jest.fn(),
    } as any as Repository<Room>;
    const studentRepo = {
      findOne: jest.fn().mockResolvedValue({ id: 3, name: '张三', organizationId: 7 }),
    } as any as Repository<Student>;
    const bedRepo = {
      findOne: jest.fn().mockResolvedValue({ id: 4, roomId: 2, status: 'available' }),
      update: jest.fn(),
    } as any as Repository<Bed>;
    const depositRepo = {
      findOne: jest.fn().mockResolvedValue(existingDeposit),
      create: jest.fn((value) => value),
      save: jest.fn(async (value) => ({ ...value, id: value?.id ?? 20 })),
    } as any as Repository<Deposit>;

    return {
      service: new OccupanciesService(
        occupancyRepo,
        roomRepo,
        studentRepo,
        depositRepo,
        bedRepo,
        { findOne: jest.fn() } as any as Repository<Locker>,
        {} as Repository<any>,
        createImportTransactionDataSource({
          occupancyRepo,
          roomRepo,
          studentRepo,
          depositRepo,
          bedRepo,
          lockerRepo: { findOne: jest.fn() } as any as Repository<Locker>,
          organizationRepo: {} as Repository<any>,
        }),
      ),
      depositRepo,
    };
  };

  const row = {
    name: '张三',
    phone: '13800138000',
    roomNumber: '4-102',
    bedNumber: '1号床',
    checkInDate: '2026-07-14',
  };



  it('rejects invalid import deposit amount before writing rows', async () => {
    const { service, depositRepo } = createImportService();

    await expect(
      service.batchImportCheckIn([row], { autoDeposit: true, depositAmount: 0 }),
    ).rejects.toThrow('押金金额必须大于0');
    expect(depositRepo.create).not.toHaveBeenCalled();
    expect(depositRepo.save).not.toHaveBeenCalled();
  });

  it('does not collect import deposit again when the student already has paid deposit', async () => {    const existing = { id: 99, amount: 500, status: 'paid' } as Deposit;
    const { service, depositRepo } = createImportService(existing);

    const result = await service.batchImportCheckIn([row], {
      autoDeposit: true,
      depositAmount: 800,
    });

    expect(result).toEqual(expect.objectContaining({ imported: 1, depositsCreated: 0 }));
    expect(depositRepo.create).not.toHaveBeenCalled();
    expect(depositRepo.save).not.toHaveBeenCalledWith(expect.objectContaining({ id: 99 }));
    expect(existing.amount).toBe(500);
  });

  it('reactivates a refunded import deposit without adding the old refunded amount', async () => {
    const existing = {
      id: 99,
      amount: 0,
      status: 'refunded',
      refundDate: '2026-07-01',
      refundAmount: 500,
      refundedBy: 11,
      refundedAt: new Date('2026-07-01T00:00:00Z'),
    } as Deposit;
    const { service, depositRepo } = createImportService(existing);

    const result = await service.batchImportCheckIn([row], {
      autoDeposit: true,
      depositAmount: 800,
    });

    expect(result).toEqual(expect.objectContaining({ imported: 1, depositsCreated: 1 }));
    expect(depositRepo.save).toHaveBeenCalledWith(
      expect.objectContaining({
        id: 99,
        amount: 800,
        status: 'paid',
        paidDate: '2026-07-14',
        refundDate: null,
        refundAmount: null,
        refundedBy: null,
        refundedAt: null,
      }),
    );
  });
});
