import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { DataSource, Repository } from 'typeorm';
import { BillsService } from './bills.service';
import { Bill } from '../entities/bill.entity';
import { BillItem } from '../entities/bill-item.entity';
import { RoomExpense } from '../entities/room-expense.entity';
import { PersonalExpense } from '../entities/personal-expense.entity';
import { Occupancy } from '../entities/occupancy.entity';
import { Room } from '../entities/room.entity';
import { Deposit } from '../entities/deposit.entity';
import { WalletsService } from '../wallets/wallets.service';

type MockRepository<T> = Partial<Record<keyof Repository<T>, jest.Mock>>;

function mockRepo<T>(): MockRepository<T> {
  return {
    find: jest.fn().mockResolvedValue([]),
    save: jest.fn().mockImplementation((entity) => Promise.resolve({ id: 1, ...entity })),
    create: jest.fn().mockImplementation((entity) => entity),
    createQueryBuilder: jest.fn().mockReturnValue({
      delete: jest.fn().mockReturnThis(),
      where: jest.fn().mockReturnThis(),
      andWhere: jest.fn().mockReturnThis(),
      execute: jest.fn().mockResolvedValue({ affected: 1 }),
    }),
    findOne: jest.fn().mockResolvedValue(null),
    remove: jest.fn().mockResolvedValue(undefined),
  };
}

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

describe('BillsService — generateBills', () => {
  let service: BillsService;
  let billRepo: MockRepository<Bill>;
  let itemRepo: MockRepository<BillItem>;
  let roomExpRepo: MockRepository<RoomExpense>;
  let personalExpRepo: MockRepository<PersonalExpense>;
  let occRepo: MockRepository<Occupancy>;
  let roomRepo: MockRepository<Room>;
  let depositRepo: MockRepository<Deposit>;
  let dataSource: { transaction: jest.Mock };

  beforeEach(async () => {
    billRepo = mockRepo<Bill>();
    itemRepo = mockRepo<BillItem>();
    roomExpRepo = mockRepo<RoomExpense>();
    personalExpRepo = mockRepo<PersonalExpense>();
    occRepo = mockRepo<Occupancy>();
    roomRepo = mockRepo<Room>();
    depositRepo = mockRepo<Deposit>();
    let nextBillId = 0;
    dataSource = {
      transaction: jest.fn(async (callback) => callback({
        create: (_entity: unknown, value: unknown) => value,
        save: jest.fn(async (value: any) => {
          if ('totalAmount' in value && 'studentId' in value) {
            const saved = { id: ++nextBillId, ...value };
            await (billRepo.save as jest.Mock)(saved);
            return saved;
          }
          await (itemRepo.save as jest.Mock)(value);
          return { id: value.id || 1, ...value };
        }),
        createQueryBuilder: jest.fn(() => ({ update: jest.fn().mockReturnThis(), set: jest.fn().mockReturnThis(), where: jest.fn().mockReturnThis(), execute: jest.fn().mockResolvedValue({ affected: 1 }) })),
      })),
      query: jest.fn().mockResolvedValue([]),
    };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        BillsService,
        { provide: getRepositoryToken(Bill), useValue: billRepo },
        { provide: getRepositoryToken(BillItem), useValue: itemRepo },
        { provide: getRepositoryToken(RoomExpense), useValue: roomExpRepo },
        { provide: getRepositoryToken(PersonalExpense), useValue: personalExpRepo },
        { provide: getRepositoryToken(Occupancy), useValue: occRepo },
        { provide: getRepositoryToken(Room), useValue: roomRepo },
        { provide: getRepositoryToken(Deposit), useValue: depositRepo },
        { provide: DataSource, useValue: dataSource },
        { provide: WalletsService, useValue: { debitBill: jest.fn(async (_manager, bill) => bill), refundBill: jest.fn() } },
      ],
    }).compile();

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

  const PERIOD = { periodStart: '2026-06-01', periodEnd: '2026-06-30' };

  it('long-term occupancy → individual bill with monthlyRate', async () => {
    // One room expense for room 1
    (roomExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<RoomExpense>([
        {
          id: 1, roomId: 1, expenseType: 'utility',
          amount: '500' as unknown as number, periodStart: '2026-06-01', periodEnd: '2026-06-30',
        } as RoomExpense,
      ]),
    );

    // One long-term occupancy in room 1
    (occRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<Occupancy>([
        {
          id: 1, studentId: 10, roomId: 1,
          billingStartDate: '2026-06-01', billingEndDate: '2026-06-30',
          stayType: 'long',
          room: { roomNumber: '101', monthlyRate: '800' as unknown as number } as Room,
        } as Occupancy,
      ]),
    );

    // No personal expenses
    (personalExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<PersonalExpense>([]),
    );

    const result = await service.generateBills(PERIOD);

    expect(result.count).toBe(1);
    const savedCalls = (billRepo.save as jest.Mock).mock.calls;
    expect(savedCalls.length).toBe(1);

    // The long-term bill should carry the monthly rate (shared = 800)
    const billData = savedCalls[0][0] as Record<string, unknown>;
    expect(Number(billData.totalAmount)).toBe(800);
    expect(Number(billData.sharedAmount)).toBe(800);
    expect(billData.studentId).toBe(10);
  });

  it('short-term occupancy → per-diem allocation pool', async () => {
    // One room expense for room 1
    (roomExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<RoomExpense>([
        {
          id: 1, roomId: 1, expenseType: 'utility',
          amount: '300' as unknown as number, periodStart: '2026-06-01', periodEnd: '2026-06-30',
        } as RoomExpense,
      ]),
    );

    // Two short-term occupancies: student 10 (10 days), student 11 (20 days) → 30 total
    (occRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<Occupancy>([
        {
          id: 1, studentId: 10, roomId: 1,
          billingStartDate: '2026-06-01', billingEndDate: '2026-06-10',
          stayType: 'short', room: undefined,
        } as Occupancy,
        {
          id: 2, studentId: 11, roomId: 1,
          billingStartDate: '2026-06-01', billingEndDate: '2026-06-20',
          stayType: 'short', room: undefined,
        } as Occupancy,
      ]),
    );

    (personalExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<PersonalExpense>([]),
    );

    const result = await service.generateBills(PERIOD);

    expect(result.count).toBe(2);
    const savedCalls = (billRepo.save as jest.Mock).mock.calls as Array<[Record<string, unknown>]>;
    const student10Bill = savedCalls.find((c) => c[0].studentId === 10);
    const student11Bill = savedCalls.find((c) => c[0].studentId === 11);

    // Student 10: 10/30 ≈ 100.00
    expect(Number(student10Bill![0].sharedAmount)).toBeCloseTo(100, 0);
    // Student 11: 20/30 ≈ 200.00
    expect(Number(student11Bill![0].sharedAmount)).toBeCloseTo(200, 0);
    // Total bill amounts sum to the expense
    expect(
      Number(student10Bill![0].totalAmount) + Number(student11Bill![0].totalAmount),
    ).toBeCloseTo(300, 0);
  });

  it('includes room expenses whose periods are inside the generated bill period', async () => {
    const qb = mockQueryBuilder<RoomExpense>([
      {
        id: 1, roomId: 1, expenseType: 'water',
        amount: '300' as unknown as number, periodStart: '2026-07-01', periodEnd: '2026-07-31',
      } as RoomExpense,
    ]);
    (roomExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(qb);

    (occRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<Occupancy>([
        {
          id: 1, studentId: 10, roomId: 1,
          billingStartDate: '2026-07-01', billingEndDate: null as unknown as string,
          stayType: 'short', room: undefined,
        } as Occupancy,
      ]),
    );

    (personalExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<PersonalExpense>([]),
    );

    const result = await service.generateBills({
      periodStart: '2026-06-29',
      periodEnd: '2026-07-31',
    });

    expect(result.count).toBe(1);
    expect(qb.where).toHaveBeenCalledWith(
      'e.periodStart >= :periodStart AND e.periodEnd <= :periodEnd',
      { periodStart: '2026-06-29', periodEnd: '2026-07-31' },
    );
    const savedCalls = (billRepo.save as jest.Mock).mock.calls as Array<[Record<string, unknown>]>;
    expect(Number(savedCalls[0][0].sharedAmount)).toBeCloseTo(300, 0);
  });

  it('mixed → long-term get individual bills, short-term share expenses', async () => {
    // Room 1: two expenses
    (roomExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<RoomExpense>([
        {
          id: 1, roomId: 1, expenseType: 'utility',
          amount: '200' as unknown as number, periodStart: '2026-06-01', periodEnd: '2026-06-30',
        } as RoomExpense,
        {
          id: 2, roomId: 1, expenseType: 'water',
          amount: '100' as unknown as number, periodStart: '2026-06-01', periodEnd: '2026-06-30',
        } as RoomExpense,
      ]),
    );

    // Mixed: one long-term (student 10) + two short-term (11, 12)
    (occRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<Occupancy>([
        {
          id: 1, studentId: 10, roomId: 1,
          billingStartDate: '2026-06-01', billingEndDate: '2026-06-30',
          stayType: 'long',
          room: { roomNumber: '101', monthlyRate: '600' as unknown as number } as Room,
        } as Occupancy,
        {
          id: 2, studentId: 11, roomId: 1,
          billingStartDate: '2026-06-01', billingEndDate: '2026-06-15',
          stayType: 'short', room: undefined,
        } as Occupancy,
        {
          id: 3, studentId: 12, roomId: 1,
          billingStartDate: '2026-06-01', billingEndDate: '2026-06-15',
          stayType: 'short', room: undefined,
        } as Occupancy,
      ]),
    );

    (personalExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<PersonalExpense>([]),
    );

    const result = await service.generateBills(PERIOD);
    const savedCalls = (billRepo.save as jest.Mock).mock.calls as Array<[Record<string, unknown>]>;

    // All 3 students get a bill
    expect(result.count).toBe(3);
    expect(savedCalls.length).toBe(3);

    // Long-term student 10 should have shared = monthlyRate (600)
    const longBill = savedCalls.find((c) => c[0].studentId === 10)!;
    expect(Number(longBill[0].sharedAmount)).toBe(600);

    // Short-term students 11 & 12 split 300 in expenses equally (15 days each)
    const short11 = savedCalls.find((c) => c[0].studentId === 11)!;
    const short12 = savedCalls.find((c) => c[0].studentId === 12)!;
    expect(Number(short11[0].sharedAmount)).toBeCloseTo(150, 0);
    expect(Number(short12[0].sharedAmount)).toBeCloseTo(150, 0);
  });

  // ============================================================
  // Bug-exposing tests
  // ============================================================

  it('long-term multi-month period multiplies and prorates monthly rent', async () => {
    // 3-month period: Jan–Mar 2026
    const THREE_MONTHS = { periodStart: '2026-01-01', periodEnd: '2026-03-31' };

    (roomExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<RoomExpense>([
        {
          id: 1, roomId: 1, expenseType: 'utility',
          amount: '500' as unknown as number, periodStart: '2026-01-01', periodEnd: '2026-03-31',
        } as RoomExpense,
      ]),
    );

    (occRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<Occupancy>([
        {
          id: 1, studentId: 10, roomId: 1,
          billingStartDate: '2026-01-01', billingEndDate: '2026-03-31',
          stayType: 'long',
          room: { roomNumber: '101', monthlyRate: '800' as unknown as number } as Room,
        } as Occupancy,
      ]),
    );

    (personalExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<PersonalExpense>([]),
    );

    const result = await service.generateBills(THREE_MONTHS);
    expect(result.count).toBe(1);

    const savedCalls = (billRepo.save as jest.Mock).mock.calls as Array<[Record<string, unknown>]>;
    const billData = savedCalls[0][0];

    // CORRECT: 800 × 3 months = 2400
    // CURRENT BUG: only 800 (monthlyRate charged once regardless of period length)
    const actual = Number(billData.totalAmount);
    const expected = 2400;

    // This assertion documents the bug — it WILL FAIL with current code
    // When the test fails, actual will be 800 instead of 2400
    expect(actual).toBeCloseTo(expected, 0);
  });

  it('long-term partial month prorates by calendar days', async () => {
    // Student occupies only Jun 15–30 (16 days out of 30), monthlyRate 600
    (roomExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<RoomExpense>([
        {
          id: 1, roomId: 1, expenseType: 'utility',
          amount: '200' as unknown as number, periodStart: '2026-06-01', periodEnd: '2026-06-30',
        } as RoomExpense,
      ]),
    );

    (occRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<Occupancy>([
        {
          id: 1, studentId: 10, roomId: 1,
          billingStartDate: '2026-06-15', billingEndDate: '2026-06-30',
          stayType: 'long',
          room: { roomNumber: '101', monthlyRate: '600' as unknown as number } as Room,
        } as Occupancy,
      ]),
    );

    (personalExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<PersonalExpense>([]),
    );

    const result = await service.generateBills(PERIOD);
    expect(result.count).toBe(1);

    const savedCalls = (billRepo.save as jest.Mock).mock.calls as Array<[Record<string, unknown>]>;
    const billData = savedCalls[0][0];

    // CORRECT: 600 × (16/30) ≈ 320
    // CURRENT BUG: 600 (full month)
    const actual = Number(billData.totalAmount);
    const expectedProrated = 320;

    expect(actual).toBeCloseTo(expectedProrated, -1);
  });

  // ============================================================
  // Correctness tests (should pass with fixed code)
  // ============================================================

  it('multiple rooms → expenses only shared within each room', async () => {
    // Room 1: 300 expense, students S10(10d) + S11(20d) = 30d total
    // Room 2: 400 expense, student S12(30d alone)
    (roomExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<RoomExpense>([
        {
          id: 1, roomId: 1, expenseType: 'utility',
          amount: '300' as unknown as number, periodStart: '2026-06-01', periodEnd: '2026-06-30',
        } as RoomExpense,
        {
          id: 2, roomId: 2, expenseType: 'utility',
          amount: '400' as unknown as number, periodStart: '2026-06-01', periodEnd: '2026-06-30',
        } as RoomExpense,
      ]),
    );

    // Use sequential query builder returns: first call → room 1 occs, second → room 2 occs
    // NOTE: mock order depends on internal service call sequence; if refactored, update callCount indices
    let callCount = 0;
    (occRepo.createQueryBuilder as jest.Mock).mockImplementation(() => {
      callCount++;
      if (callCount === 1) {
        return mockQueryBuilder<Occupancy>([
          {
            id: 1, studentId: 10, roomId: 1,
            billingStartDate: '2026-06-01', billingEndDate: '2026-06-10',
            stayType: 'short', room: undefined,
          } as Occupancy,
          {
            id: 2, studentId: 11, roomId: 1,
            billingStartDate: '2026-06-01', billingEndDate: '2026-06-20',
            stayType: 'short', room: undefined,
          } as Occupancy,
        ]);
      }
      return mockQueryBuilder<Occupancy>([
        {
          id: 3, studentId: 12, roomId: 2,
          billingStartDate: '2026-06-01', billingEndDate: '2026-06-30',
          stayType: 'short', room: undefined,
        } as Occupancy,
      ]);
    });

    (personalExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<PersonalExpense>([]),
    );

    // Mock student department query
    (dataSource.query as jest.Mock).mockResolvedValue([
      { id: 10, department_id: null },
      { id: 11, department_id: null },
      { id: 12, department_id: null },
    ]);

    const result = await service.generateBills(PERIOD);
    expect(result.count).toBe(3);

    const savedCalls = (billRepo.save as jest.Mock).mock.calls as Array<[Record<string, unknown>]>;
    const s10Bill = savedCalls.find((c) => c[0].studentId === 10)!;
    const s11Bill = savedCalls.find((c) => c[0].studentId === 11)!;
    const s12Bill = savedCalls.find((c) => c[0].studentId === 12)!;

    // Room 2: S12 alone → pays all 400
    expect(Number(s12Bill[0].totalAmount)).toBeCloseTo(400, 0);

    // Room 1: S10 10/30 ≈ 100, S11 20/30 ≈ 200
    expect(Number(s10Bill[0].sharedAmount)).toBeCloseTo(100, 0);
    expect(Number(s11Bill[0].sharedAmount)).toBeCloseTo(200, 0);

    // Total across all rooms
    const totalAll = [s10Bill, s11Bill, s12Bill].reduce(
      (sum, c) => sum + Number(c[0].totalAmount), 0,
    );
    expect(totalAll).toBeCloseTo(700, 0);
  });

  it('personal expenses → added on top of shared allocation', async () => {
    (roomExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<RoomExpense>([
        {
          id: 1, roomId: 1, expenseType: 'utility',
          amount: '300' as unknown as number, periodStart: '2026-06-01', periodEnd: '2026-06-30',
        } as RoomExpense,
      ]),
    );

    (occRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<Occupancy>([
        {
          id: 1, studentId: 10, roomId: 1,
          billingStartDate: '2026-06-01', billingEndDate: '2026-06-30',
          stayType: 'short', room: undefined,
        } as Occupancy,
      ]),
    );

    // Personal expense: damage fee of 50
    (personalExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<PersonalExpense>([
        {
          id: 1, studentId: 10, roomId: 1,
          expenseType: 'damage', amount: '50' as unknown as number,
          expenseDate: '2026-06-15', description: 'broken chair',
        } as PersonalExpense,
      ]),
    );

    const result = await service.generateBills(PERIOD);
    expect(result.count).toBe(1);

    const savedCalls = (billRepo.save as jest.Mock).mock.calls as Array<[Record<string, unknown>]>;
    const billData = savedCalls[0][0];

    expect(Number(billData.sharedAmount)).toBeCloseTo(300, 0);
    expect(Number(billData.personalAmount)).toBe(50);
    expect(Number(billData.totalAmount)).toBeCloseTo(350, 0);
  });

  it('zero overlapping days → no bill generated', async () => {
    (roomExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<RoomExpense>([
        {
          id: 1, roomId: 1, expenseType: 'utility',
          amount: '300' as unknown as number, periodStart: '2026-06-01', periodEnd: '2026-06-30',
        } as RoomExpense,
      ]),
    );

    // Occupancy starts AFTER period ends — no overlap
    (occRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<Occupancy>([
        {
          id: 1, studentId: 10, roomId: 1,
          billingStartDate: '2026-07-01', billingEndDate: '2026-07-15',
          stayType: 'short', room: undefined,
        } as Occupancy,
      ]),
    );

    (personalExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<PersonalExpense>([]),
    );

    const result = await service.generateBills(PERIOD);
    // Occupancy outside period → no matching student days → no bill
    expect(result.count).toBe(0);
  });

  it('no room expenses → no bills generated', async () => {
    (roomExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<RoomExpense>([]),
    );

    (occRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<Occupancy>([
        {
          id: 1, studentId: 10, roomId: 1,
          billingStartDate: '2026-06-01', billingEndDate: '2026-06-30',
          stayType: 'short', room: undefined,
        } as Occupancy,
      ]),
    );

    (personalExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(
      mockQueryBuilder<PersonalExpense>([]),
    );

    const result = await service.generateBills(PERIOD);
    expect(result.count).toBe(0);
  });
});

describe('BillsService — allocation rounding boundary', () => {
  it('keeps allocated cents equal to the original expense total', async () => {
    const billRepo = mockRepo<Bill>();
    const itemRepo = mockRepo<BillItem>();
    const roomExpRepo = mockRepo<RoomExpense>();
    const personalExpRepo = mockRepo<PersonalExpense>();
    const occRepo = mockRepo<Occupancy>();
    const roomRepo = mockRepo<Room>();
    let nextBillId = 0;
    const dataSource = {
      query: jest.fn().mockResolvedValue([]),
      transaction: jest.fn(async (callback) => callback({
        create: (_entity: unknown, value: any) => value,
        save: jest.fn(async (value: any) => ({ id: value.id || ++nextBillId, ...value })),
        createQueryBuilder: jest.fn(() => ({
          update: jest.fn().mockReturnThis(),
          set: jest.fn().mockReturnThis(),
          where: jest.fn().mockReturnThis(),
          execute: jest.fn().mockResolvedValue({ affected: 1 }),
        })),
      })),
    };
    const service = new BillsService(
      billRepo as any,
      itemRepo as any,
      roomExpRepo as any,
      personalExpRepo as any,
      occRepo as any,
      roomRepo as any,
      dataSource as any,
      { debitBill: jest.fn(async (_manager, bill) => bill) } as any,
    );
    (roomExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(mockQueryBuilder<RoomExpense>([
      { id: 1, roomId: 1, expenseType: 'water', amount: 100, periodStart: '2026-06-01', periodEnd: '2026-06-30' } as RoomExpense,
    ]));
    (occRepo.createQueryBuilder as jest.Mock).mockReturnValue(mockQueryBuilder<Occupancy>([
      { id: 1, roomId: 1, studentId: 1, stayType: 'short', billingStartDate: '2026-06-01', billingEndDate: '2026-06-01' } as Occupancy,
      { id: 2, roomId: 1, studentId: 2, stayType: 'short', billingStartDate: '2026-06-01', billingEndDate: '2026-06-01' } as Occupancy,
      { id: 3, roomId: 1, studentId: 3, stayType: 'short', billingStartDate: '2026-06-01', billingEndDate: '2026-06-01' } as Occupancy,
    ]));
    (personalExpRepo.createQueryBuilder as jest.Mock).mockReturnValue(mockQueryBuilder<PersonalExpense>([]));

    const result = await service.generateBills({ periodStart: '2026-06-01', periodEnd: '2026-06-30' } as any);

    expect(result.bills.map((bill) => Number(bill.totalAmount))).toEqual([33.33, 33.33, 33.34]);
    expect(result.bills.reduce((sum, bill) => sum + Number(bill.totalAmount), 0)).toBe(100);
  });
});
