/**
 * Late-fine computation tests.
 *
 * Exercises recalculateFines() on in-memory assignment documents, with a
 * mocked structureModel, to verify:
 *   - Backdated fees (admin created AFTER the due date) still accrue fines
 *   - Future-dated assignments don't accrue fines
 *   - Per-day / percentage / day-wise-slab modes
 *   - Fine computed on REMAINING unpaid amount (base + subFees − paid)
 *   - lateFine.enabled = false short-circuits
 *   - Deleted structure short-circuits (no structure found)
 */

import { Test } from '@nestjs/testing';
import { getModelToken } from '@nestjs/mongoose';
import { FeesService } from './fees.service';
import { FeeStructure } from './schemas/fee-structure.schema';
import { FeeAssignment } from './schemas/fee-assignment.schema';
import { FeePayment } from './schemas/fee-payment.schema';
import { Student } from '../students/schemas/student.schema';
import { AcademicYear } from '../academic-year/academic-year.schema';
import { Admin } from '../admin/schemas/admin.schema';
import { InvoicesService } from '../invoices/invoices.service';

/** Make a no-op model stub. Individual tests override methods as needed. */
function stubModel(overrides: any = {}): any {
    const chain = (result: any) => ({ lean: () => Promise.resolve(result) });
    return {
        find: jest.fn().mockReturnValue(chain([])),
        findOne: jest.fn().mockReturnValue(chain(null)),
        findById: jest.fn().mockReturnValue(chain(null)),
        create: jest.fn(),
        insertMany: jest.fn(),
        bulkWrite: jest.fn(),
        findByIdAndUpdate: jest.fn().mockReturnValue(chain(null)),
        findOneAndUpdate: jest.fn().mockReturnValue(chain(null)),
        findByIdAndDelete: jest.fn(),
        deleteMany: jest.fn(),
        countDocuments: jest.fn().mockResolvedValue(0),
        exists: jest.fn().mockResolvedValue(false),
        distinct: jest.fn().mockResolvedValue([]),
        ...overrides,
    };
}

async function makeService(structureLookup: Record<string, any>): Promise<FeesService> {
    const structureModel = stubModel({
        find: jest.fn().mockImplementation(({ _id }: any) => ({
            lean: async () => {
                const ids = _id?.$in || [];
                return ids.map((id: string) => structureLookup[String(id)]).filter(Boolean);
            },
        })),
    });

    const mod = await Test.createTestingModule({
        providers: [
            FeesService,
            { provide: getModelToken(FeeStructure.name), useValue: structureModel },
            { provide: getModelToken(FeeAssignment.name), useValue: stubModel() },
            { provide: getModelToken(Student.name), useValue: stubModel() },
            { provide: getModelToken(AcademicYear.name), useValue: stubModel() },
            { provide: getModelToken(Admin.name), useValue: stubModel() },
            { provide: getModelToken(FeePayment.name), useValue: stubModel() },
            { provide: InvoicesService, useValue: { upsertFromFeePayment: jest.fn().mockResolvedValue(null) } },
        ],
    }).compile();
    return mod.get(FeesService);
}

// Freeze "now" at a fixed date so tests are deterministic
const NOW = new Date('2026-04-15T00:00:00Z');
beforeAll(() => { jest.useFakeTimers().setSystemTime(NOW); });
afterAll(() => { jest.useRealTimers(); });

describe('recalculateFines — backdated fees', () => {
    it('charges retroactive fine for a past due-date even when assignment was created AFTER dueDate', async () => {
        const structureId = 's1';
        const service = await makeService({
            [structureId]: {
                _id: structureId,
                lateFine: { enabled: true, type: 'per_day', amountPerDay: 50 },
            },
        });

        // Admin created the fee on Apr 10 (today is Apr 15) with a Jan 10 due date.
        const assignment = {
            _id: 'a1',
            feeStructureId: structureId,
            amount: 5000,
            baseAmount: 5000,
            subFees: [],
            paidAmount: 0,
            fineAmount: 0,
            status: 'pending',
            dueDate: new Date('2026-01-10T00:00:00Z'),
            createdAt: new Date('2026-04-10T00:00:00Z'), // AFTER dueDate
        };

        const [out] = await service.recalculateFines([assignment]);
        // daysLate = Apr 15 − Jan 10 = 95 days → 95 × 50 = 4750
        expect(out.fineAmount).toBe(4750);
        expect(out.status).toBe('overdue');
    });

    it('percentage mode: 10% of remaining base', async () => {
        const service = await makeService({
            s1: { _id: 's1', lateFine: { enabled: true, type: 'percentage', percentage: 10 } },
        });
        const [out] = await service.recalculateFines([
            {
                _id: 'a1', feeStructureId: 's1', amount: 5000, paidAmount: 0,
                status: 'pending', dueDate: new Date('2026-01-10T00:00:00Z'),
                createdAt: new Date('2026-04-10T00:00:00Z'),
            },
        ]);
        expect(out.fineAmount).toBe(500);
    });

    it('fine on REMAINING base, not full amount (partial paid row)', async () => {
        const service = await makeService({
            s1: { _id: 's1', lateFine: { enabled: true, type: 'percentage', percentage: 10 } },
        });
        const [out] = await service.recalculateFines([
            {
                _id: 'a1', feeStructureId: 's1', amount: 5000, paidAmount: 3000,
                status: 'partial', dueDate: new Date('2026-01-10T00:00:00Z'),
                createdAt: new Date('2026-04-10T00:00:00Z'),
            },
        ]);
        // 10% of remaining (2000) = 200
        expect(out.fineAmount).toBe(200);
    });

    it('no fine if fully paid against base (remainingBase ≤ 0)', async () => {
        const service = await makeService({
            s1: { _id: 's1', lateFine: { enabled: true, type: 'percentage', percentage: 10 } },
        });
        const [out] = await service.recalculateFines([
            {
                _id: 'a1', feeStructureId: 's1', amount: 5000, paidAmount: 5000,
                status: 'partial', dueDate: new Date('2026-01-10T00:00:00Z'),
                createdAt: new Date('2026-04-10T00:00:00Z'),
            },
        ]);
        expect(out.fineAmount).toBe(0);
    });

    it('no fine for future-dated assignments (dueDate > now)', async () => {
        const service = await makeService({
            s1: { _id: 's1', lateFine: { enabled: true, type: 'per_day', amountPerDay: 50 } },
        });
        const [out] = await service.recalculateFines([
            {
                _id: 'a1', feeStructureId: 's1', amount: 5000, paidAmount: 0,
                status: 'pending', dueDate: new Date('2026-05-10T00:00:00Z'), // FUTURE
                createdAt: new Date('2026-04-10T00:00:00Z'),
            },
        ]);
        expect(out.fineAmount).toBe(0);
        expect(out.status).toBe('pending');
    });

    it('no fine if lateFine.enabled = false on the structure', async () => {
        const service = await makeService({
            s1: { _id: 's1', lateFine: { enabled: false, type: 'per_day', amountPerDay: 50 } },
        });
        const [out] = await service.recalculateFines([
            {
                _id: 'a1', feeStructureId: 's1', amount: 5000, paidAmount: 0,
                status: 'pending', dueDate: new Date('2026-01-10T00:00:00Z'),
                createdAt: new Date('2026-04-10T00:00:00Z'),
            },
        ]);
        expect(out.fineAmount).toBe(0);
        expect(out.status).toBe('overdue');
    });

    it('no fine if parent structure was deleted (not in lookup)', async () => {
        const service = await makeService({}); // empty lookup
        const [out] = await service.recalculateFines([
            {
                _id: 'a1', feeStructureId: 's-deleted', amount: 5000, paidAmount: 0,
                status: 'pending', dueDate: new Date('2026-01-10T00:00:00Z'),
                createdAt: new Date('2026-04-10T00:00:00Z'),
            },
        ]);
        expect(out.fineAmount).toBe(0);
        expect(out.status).toBe('overdue');
    });

    it('paid rows keep stored fineAmount (no recompute)', async () => {
        const service = await makeService({
            s1: { _id: 's1', lateFine: { enabled: true, type: 'percentage', percentage: 10 } },
        });
        const [out] = await service.recalculateFines([
            {
                _id: 'a1', feeStructureId: 's1', amount: 5000, paidAmount: 5500,
                fineAmount: 250, // stored at payment time
                status: 'paid', dueDate: new Date('2026-01-10T00:00:00Z'),
                createdAt: new Date('2026-04-10T00:00:00Z'),
            },
        ]);
        expect(out.fineAmount).toBe(250);
        expect(out.status).toBe('paid');
    });

    it('day-wise slab: picks matching slab by daysLate', async () => {
        const service = await makeService({
            s1: {
                _id: 's1',
                lateFine: {
                    enabled: true, type: 'day_wise_slab',
                    slabs: [
                        { startDay: 1, endDay: 10, amount: 100 },
                        { startDay: 11, endDay: 30, amount: 300 },
                        { startDay: 31, endDay: 90, amount: 800 },
                    ],
                },
            },
        });
        // daysLate = 95 → exceeds last slab → use last slab (800)
        const [out1] = await service.recalculateFines([
            {
                _id: 'a1', feeStructureId: 's1', amount: 5000, paidAmount: 0,
                status: 'pending', dueDate: new Date('2026-01-10T00:00:00Z'),
            },
        ]);
        expect(out1.fineAmount).toBe(800);

        // daysLate = 5 → first slab → 100
        const [out2] = await service.recalculateFines([
            {
                _id: 'a2', feeStructureId: 's1', amount: 5000, paidAmount: 0,
                status: 'pending', dueDate: new Date('2026-04-10T00:00:00Z'),
            },
        ]);
        expect(out2.fineAmount).toBe(100);
    });

    it('sub-fees: fine is computed on TOTAL (base + subFees), not base only', async () => {
        const service = await makeService({
            s1: { _id: 's1', lateFine: { enabled: true, type: 'percentage', percentage: 10 } },
        });
        const [out] = await service.recalculateFines([
            {
                _id: 'a1', feeStructureId: 's1',
                amount: 6000,      // total (base + subFees)
                baseAmount: 5000,
                subFees: [{ name: 'Library', amount: 1000 }],
                paidAmount: 0,
                status: 'pending',
                dueDate: new Date('2026-01-10T00:00:00Z'),
                createdAt: new Date('2026-04-10T00:00:00Z'),
            },
        ]);
        // 10% of 6000 = 600
        expect(out.fineAmount).toBe(600);
    });
});
