import 'reflect-metadata';
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';
import { CreateFeeStructureDto } from './dto/create-fee-structure.dto';

const base = {
    name: 'Test',
    feeType: 'monthly' as const,
    academicYearId: '507f1f77bcf86cd799439011',
    amount: 100,
    classIds: ['all'],
    dueDay: 10,
};

async function errCount(payload: any): Promise<number> {
    const dto = plainToInstance(CreateFeeStructureDto, payload);
    const errors = await validate(dto);
    return errors.length;
}

async function firstError(payload: any): Promise<string> {
    const dto = plainToInstance(CreateFeeStructureDto, payload);
    const errors = await validate(dto);
    return errors[0] ? JSON.stringify(errors[0].constraints) : 'none';
}

describe('CreateFeeStructureDto validation probe', () => {
    it('valid ISO dates pass', async () => {
        expect(await errCount({ ...base, startDate: '2027-01-01', endDate: '2027-12-31' })).toBe(0);
    });

    it('no dates passes (both optional)', async () => {
        expect(await errCount({ ...base })).toBe(0);
    });

    it('endDate only (no startDate) passes', async () => {
        expect(await errCount({ ...base, endDate: '2027-12-31' })).toBe(0);
    });

    it('startDate = "not a date" FAILS @IsDateString', async () => {
        expect(await errCount({ ...base, startDate: 'not a date' })).toBeGreaterThan(0);
    });

    it('startDate = "2027-13-45" (invalid calendar date) FAILS @IsDateString', async () => {
        expect(await errCount({ ...base, startDate: '2027-13-45' })).toBeGreaterThan(0);
    });

    it('startDate = "2027-02-30" (Feb 30, out-of-month) PASSES @IsDateString — known class-validator limitation; Date() coerces to Mar 2', async () => {
        // @IsDateString only validates ISO 8601 format, not calendar validity.
        // "2027-02-30" is format-valid but `new Date("2027-02-30")` = Mar 2, 2027.
        // Low risk — DatePicker in UI prevents manual entry of bad days; only
        // malicious/direct-API callers can produce this, and the effect is a
        // 2-day date shift, not a crash.
        expect(await errCount({ ...base, startDate: '2027-02-30' })).toBe(0);
    });

    it('startDate = "2027-01-01T00:00:00Z" (ISO with time) PASSES', async () => {
        expect(await errCount({ ...base, startDate: '2027-01-01T00:00:00Z' })).toBe(0);
    });

    it('startDate = "" (empty string) FAILS @IsDateString', async () => {
        // Documented outcome: class-validator's @IsDateString rejects empty
        // strings. The frontend DatePicker emits "" when cleared, and the
        // form maps `v || undefined` before posting, so the empty case should
        // never reach the backend. This test guards against regressions in
        // the form's coercion + the DTO decorator's behavior.
        const n = await errCount({ ...base, startDate: '' });
        expect(n).toBeGreaterThan(0);
    });

    it('startDate = number fails @IsString', async () => {
        const n = await errCount({ ...base, startDate: 12345 });
        expect(n).toBeGreaterThan(0);
    });

    it('endDate before startDate FAILS via EndAfterStartValidator', async () => {
        const err = await firstError({ ...base, startDate: '2027-06-01', endDate: '2027-01-01' });
        expect(err).toContain('endAfterStart');
    });

    it('endDate == startDate FAILS (strict > not >=)', async () => {
        const err = await firstError({ ...base, startDate: '2027-06-01', endDate: '2027-06-01' });
        expect(err).toContain('endAfterStart');
    });

    it('dueDay=0 fails @Min(1)', async () => {
        const n = await errCount({ ...base, dueDay: 0 });
        expect(n).toBeGreaterThan(0);
    });

    it('dueDay=32 fails @Max(31)', async () => {
        const n = await errCount({ ...base, dueDay: 32 });
        expect(n).toBeGreaterThan(0);
    });

    it('amount=0 fails @Min(1)', async () => {
        const n = await errCount({ ...base, amount: 0 });
        expect(n).toBeGreaterThan(0);
    });

    it('amount=10_000_001 fails @Max(10000000)', async () => {
        const n = await errCount({ ...base, amount: 10_000_001 });
        expect(n).toBeGreaterThan(0);
    });

    it('feeType="invalid" fails @IsIn', async () => {
        const n = await errCount({ ...base, feeType: 'invalid' });
        expect(n).toBeGreaterThan(0);
    });

    it('classIds=[] fails @ArrayMinSize(1)', async () => {
        const n = await errCount({ ...base, classIds: [] });
        expect(n).toBeGreaterThan(0);
    });

    it('invalid academicYearId fails @IsMongoId', async () => {
        const n = await errCount({ ...base, academicYearId: 'nope' });
        expect(n).toBeGreaterThan(0);
    });

    // --- Sub-fees ---
    it('subFees undefined passes (optional)', async () => {
        expect(await errCount({ ...base })).toBe(0);
    });

    it('subFees=[] passes', async () => {
        expect(await errCount({ ...base, subFees: [], enableSubFees: true })).toBe(0);
    });

    it('valid subFees pass', async () => {
        expect(await errCount({ ...base, enableSubFees: true, subFees: [{ name: 'Library', amount: 500 }] })).toBe(0);
    });

    it('subFee with empty name FAILS @IsNotEmpty', async () => {
        expect(await errCount({ ...base, subFees: [{ name: '', amount: 500 }] })).toBeGreaterThan(0);
    });

    it('subFee with missing name FAILS', async () => {
        expect(await errCount({ ...base, subFees: [{ amount: 500 }] })).toBeGreaterThan(0);
    });

    it('subFee with name > 200 chars FAILS @MaxLength', async () => {
        expect(await errCount({ ...base, subFees: [{ name: 'x'.repeat(201), amount: 500 }] })).toBeGreaterThan(0);
    });

    it('subFee with negative amount FAILS @Min(0)', async () => {
        expect(await errCount({ ...base, subFees: [{ name: 'x', amount: -1 }] })).toBeGreaterThan(0);
    });

    it('subFee amount=0 passes @Min(0)', async () => {
        expect(await errCount({ ...base, subFees: [{ name: 'x', amount: 0 }] })).toBe(0);
    });

    it('subFee amount > 10M FAILS @Max(10000000)', async () => {
        expect(await errCount({ ...base, subFees: [{ name: 'x', amount: 10_000_001 }] })).toBeGreaterThan(0);
    });

    it('subFees array of 51 FAILS @ArrayMaxSize(50)', async () => {
        const big = Array.from({ length: 51 }, (_, i) => ({ name: `f${i}`, amount: 100 }));
        expect(await errCount({ ...base, subFees: big })).toBeGreaterThan(0);
    });

    it('subFees array of 50 passes @ArrayMaxSize(50)', async () => {
        const big = Array.from({ length: 50 }, (_, i) => ({ name: `f${i}`, amount: 100 }));
        expect(await errCount({ ...base, enableSubFees: true, subFees: big })).toBe(0);
    });

    it('subFees with extra property stripped by whitelist (still valid)', async () => {
        // class-validator doesn't enforce whitelist on its own — ValidationPipe does.
        // This test ensures the DTO at least doesn't reject a known-valid subFee.
        expect(await errCount({ ...base, subFees: [{ name: 'x', amount: 100 }] })).toBe(0);
    });
});
