/**
 * Integration tests that wire sub-fees, backdated fines, and assignment
 * generation together to catch cross-feature regressions.
 *
 * Exercises:
 *   - buildAssignmentDocs (via generateAssignments→insertMany) with sub-fees enabled
 *   - recalculateFines on the generated docs with a past startDate (retroactive fine)
 *   - enrichAssignments computes netPayable = amount + fine − discount − paid
 *   - Legacy assignment shape (no baseAmount/subFees) still read-compatible
 */

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';

const NOW = new Date('2026-04-15T00:00:00Z');
beforeAll(() => { jest.useFakeTimers().setSystemTime(NOW); });
afterAll(() => { jest.useRealTimers(); });

function stubModel(overrides: any = {}): any {
    const chain = (result: any) => ({ lean: () => Promise.resolve(result) });
    return {
        find: jest.fn().mockReturnValue({ ...chain([]), sort: () => chain([]), limit: () => chain([]) }),
        findOne: jest.fn().mockReturnValue(chain(null)),
        findById: jest.fn().mockReturnValue(chain(null)),
        create: jest.fn(),
        insertMany: jest.fn().mockResolvedValue([]),
        bulkWrite: jest.fn().mockResolvedValue({ upsertedCount: 0, modifiedCount: 0 }),
        findByIdAndUpdate: jest.fn().mockReturnValue(chain(null)),
        findOneAndUpdate: jest.fn().mockReturnValue(chain(null)),
        deleteMany: jest.fn(),
        countDocuments: jest.fn().mockResolvedValue(0),
        exists: jest.fn().mockResolvedValue(false),
        distinct: jest.fn().mockResolvedValue([]),
        ...overrides,
    };
}

async function makeService(opts: {
    structures?: Record<string, any>;
    studentsByClass?: Record<string, any[]>;
    academicYears?: Record<string, any>;
    onInsert?: (docs: any[]) => void;
}): Promise<FeesService> {
    const structureModel = stubModel({
        find: jest.fn().mockImplementation((q: any = {}) => {
            const ids = q._id?.$in;
            const ay = q.academicYearId;
            const docs = Object.values(opts.structures || {}).filter((s: any) => {
                if (ids && !ids.includes(String(s._id))) return false;
                if (ay && s.academicYearId !== ay) return false;
                return true;
            });
            return {
                lean: () => Promise.resolve(docs),
                sort: () => ({ lean: () => Promise.resolve(docs), limit: () => ({ lean: () => Promise.resolve(docs) }) }),
                limit: () => ({ lean: () => Promise.resolve(docs) }),
            };
        }),
        findById: jest.fn().mockImplementation((id: string) => ({
            lean: () => Promise.resolve(opts.structures?.[String(id)] || null),
        })),
    });

    const studentModel = stubModel({
        find: jest.fn().mockImplementation((q: any) => {
            const classIds = q.classId?.$in;
            const students = classIds
                ? classIds.flatMap((cid: string) => opts.studentsByClass?.[cid] || [])
                : Object.values(opts.studentsByClass || {}).flat();
            return { lean: () => Promise.resolve(students), limit: () => ({ lean: () => Promise.resolve(students) }) };
        }),
    });

    const academicYearModel = stubModel({
        findById: jest.fn().mockImplementation((id: string) => ({
            lean: () => Promise.resolve(opts.academicYears?.[String(id)] || null),
        })),
    });

    const assignmentModel = stubModel({
        insertMany: jest.fn().mockImplementation((docs: any[]) => {
            opts.onInsert?.(docs);
            return Promise.resolve(docs);
        }),
    });

    const mod = await Test.createTestingModule({
        providers: [
            FeesService,
            { provide: getModelToken(FeeStructure.name), useValue: structureModel },
            { provide: getModelToken(FeeAssignment.name), useValue: assignmentModel },
            { provide: getModelToken(Student.name), useValue: studentModel },
            { provide: getModelToken(AcademicYear.name), useValue: academicYearModel },
            { 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);
}

describe('integration — sub-fees + backdated fines', () => {
    it('generates monthly assignments for a backdated fee with sub-fees and retroactive fines', async () => {
        let inserted: any[] = [];
        const service = await makeService({
            studentsByClass: { c1: [{ _id: 'stu1', classId: 'c1' }] },
            academicYears: {
                ay1: { _id: 'ay1', startDate: new Date('2025-06-01'), endDate: new Date('2026-05-31') },
            },
            onInsert: (docs) => { inserted = docs; },
        });

        // Expose the private method to directly verify the doc shape. This
        // side-steps the need to wire a full createStructure flow.
        const structure = {
            _id: 's1',
            name: 'Tuition',
            feeType: 'monthly' as const,
            academicYearId: 'ay1',
            amount: 5000,
            classIds: ['c1'],
            dueMonth: 1,
            dueDay: 10,
            startDate: new Date('2026-01-01'),  // BACKDATED
            enableSubFees: true,
            subFees: [
                { name: 'Library', amount: 500 },
                { name: 'Sports', amount: 300 },
            ],
        };
        const docs = (service as any).buildAssignmentDocs(
            structure,
            [{ _id: 'stu1', classId: 'c1' }],
            new Date('2026-01-01'),
            new Date('2026-05-31'),
        );

        // Should emit one installment per month from Jan to May (startDate to end)
        expect(docs.length).toBeGreaterThanOrEqual(4);
        for (const doc of docs) {
            expect(doc.amount).toBe(5800);          // total = base + subFees
            expect(doc.baseAmount).toBe(5000);      // base preserved
            expect(doc.subFees).toEqual([
                { name: 'Library', amount: 500 },
                { name: 'Sports', amount: 300 },
            ]);
            expect(doc.fineAmount).toBe(0);          // fine NOT stored at build time
            expect(doc.status).toBe('pending');
        }
    });

    it('recalculateFines computes retroactive fine on total (base + subFees) for backdated assignments', async () => {
        const structure = {
            _id: 's1',
            lateFine: { enabled: true, type: 'per_day', amountPerDay: 50 },
        };
        const service = await makeService({ structures: { s1: structure } });

        const assignments = [
            {
                _id: 'a-jan', feeStructureId: 's1', amount: 5800, baseAmount: 5000,
                subFees: [{ name: 'Library', amount: 500 }, { name: 'Sports', amount: 300 }],
                paidAmount: 0, fineAmount: 0, status: 'pending',
                dueDate: new Date('2026-01-10'), createdAt: new Date('2026-04-10'),
            },
            {
                _id: 'a-feb', feeStructureId: 's1', amount: 5800, baseAmount: 5000,
                subFees: [{ name: 'Library', amount: 500 }, { name: 'Sports', amount: 300 }],
                paidAmount: 0, fineAmount: 0, status: 'pending',
                dueDate: new Date('2026-02-10'), createdAt: new Date('2026-04-10'),
            },
        ];

        const out = await service.recalculateFines(assignments);
        // Jan: 95 days × 50 = 4750
        // Feb: 64 days × 50 = 3200
        expect(out[0].fineAmount).toBe(4750);
        expect(out[0].status).toBe('overdue');
        expect(out[1].fineAmount).toBe(3200);
        expect(out[1].status).toBe('overdue');
        // Sub-fees preserved through read
        expect(out[0].subFees).toHaveLength(2);
        expect(out[0].baseAmount).toBe(5000);
    });

    it('legacy assignment without baseAmount/subFees reads correctly', async () => {
        const service = await makeService({
            structures: { s1: { _id: 's1', lateFine: { enabled: true, type: 'percentage', percentage: 10 } } },
        });
        // Simulates a pre-migration row: no baseAmount, no subFees
        const [out] = await service.recalculateFines([
            {
                _id: 'legacy', feeStructureId: 's1',
                amount: 5000, // base only; subFees array not present
                paidAmount: 0, fineAmount: 0, status: 'pending',
                dueDate: new Date('2026-01-10'), createdAt: new Date('2026-01-05'),
            },
        ]);
        expect(out.fineAmount).toBe(500); // 10% of 5000
        // Legacy row has no baseAmount/subFees — that's OK, frontend uses
        // `a.baseAmount ?? a.amount` and `a.subFees ?? []` for display.
        expect(out.baseAmount).toBeUndefined();
        expect(out.subFees).toBeUndefined();
    });

    it('lateFine disabled: no fine even if backdated + sub-fees', async () => {
        const service = await makeService({
            structures: { s1: { _id: 's1', lateFine: { enabled: false } } },
        });
        const [out] = await service.recalculateFines([
            {
                _id: 'a1', feeStructureId: 's1', amount: 5800, baseAmount: 5000,
                subFees: [{ name: 'Library', amount: 500 }, { name: 'Sports', amount: 300 }],
                paidAmount: 0, fineAmount: 0, status: 'pending',
                dueDate: new Date('2026-01-10'), createdAt: new Date('2026-04-10'),
            },
        ]);
        expect(out.fineAmount).toBe(0);
        expect(out.status).toBe('overdue');
    });

    it('partial paid row: fine recomputed on remaining (amount − paid)', async () => {
        const service = await makeService({
            structures: { s1: { _id: 's1', lateFine: { enabled: true, type: 'percentage', percentage: 10 } } },
        });
        const [out] = await service.recalculateFines([
            {
                _id: 'a1', feeStructureId: 's1', amount: 5800, baseAmount: 5000,
                subFees: [{ name: 'Library', amount: 500 }, { name: 'Sports', amount: 300 }],
                paidAmount: 2800, // paid part of sub-fees + part of base
                fineAmount: 100, // stored at original partial-pay time
                status: 'partial',
                dueDate: new Date('2026-01-10'),
            },
        ]);
        // remaining = 5800 − 2800 = 3000 → 10% = 300
        expect(out.fineAmount).toBe(300);
        expect(out.status).toBe('overdue');
    });

    it('sub-fees disabled on structure with stale subFees array → total = base only', async () => {
        // Admin disabled enableSubFees but didn't clear the subFees array (possible
        // via malformed API call). buildAssignmentDocs must respect enableSubFees.
        const service = await makeService({});
        const structure = {
            _id: 's1', name: 'Tuition', feeType: 'one-time' as const,
            academicYearId: 'ay1', amount: 5000, classIds: ['all'], dueDay: 10, dueMonth: 1,
            enableSubFees: false, // disabled
            subFees: [{ name: 'Library', amount: 500 }], // but array has stale data
        };
        const docs = (service as any).buildAssignmentDocs(
            structure,
            [{ _id: 'stu1', classId: 'c1' }],
            new Date('2026-01-01'),
            new Date('2026-05-31'),
        );
        expect(docs[0].amount).toBe(5000);       // base only
        expect(docs[0].baseAmount).toBe(5000);
        expect(docs[0].subFees).toEqual([]);     // stale array dropped
    });

    it('enableSubFees=true with empty subFees array → total = base', async () => {
        const service = await makeService({});
        const structure = {
            _id: 's1', name: 'Tuition', feeType: 'one-time' as const,
            academicYearId: 'ay1', amount: 5000, classIds: ['all'], dueDay: 10, dueMonth: 1,
            enableSubFees: true,
            subFees: [],
        };
        const docs = (service as any).buildAssignmentDocs(
            structure,
            [{ _id: 'stu1', classId: 'c1' }],
            new Date('2026-01-01'),
            new Date('2026-05-31'),
        );
        expect(docs[0].amount).toBe(5000);
        expect(docs[0].baseAmount).toBe(5000);
        expect(docs[0].subFees).toEqual([]);
    });
});
