/**
 * Locks the Razorpay credentials refresh in TransportService.
 * Mirrors the pattern from invoices/fees/payments — admin updates via Settings UI
 * must take effect on the next payment operation without restart.
 */
import { Test } from '@nestjs/testing';
import { getModelToken } from '@nestjs/mongoose';
import { BadRequestException } from '@nestjs/common';
import { TransportService } from './transport.service';
import { TransportRoute } from './schemas/transport-route.schema';
import { TransportAssignment } from './schemas/transport-assignment.schema';
import { Student } from '../students/schemas/student.schema';
import { AcademicYear } from '../academic-year/academic-year.schema';
import { AuditService } from '../audit/audit.service';
import { SettingsService } from '../settings/settings.service';

jest.mock('razorpay', () => jest.fn().mockImplementation((opts: any) => ({
    __key_id: opts.key_id,
    __key_secret: opts.key_secret,
    orders: { create: jest.fn() },
    payments: { fetch: jest.fn() },
})));

function stubModel() {
    return {
        find: jest.fn(),
        findOne: jest.fn(),
        findById: jest.fn(),
        findOneAndUpdate: jest.fn(),
        findByIdAndUpdate: jest.fn(),
        create: jest.fn(),
        countDocuments: jest.fn(),
        exists: jest.fn(),
    };
}

async function makeService(creds: { keyId: string; keySecret: string } | (() => { keyId: string; keySecret: string })): Promise<TransportService> {
    const credsFn = typeof creds === 'function' ? creds : () => creds;
    const mod = await Test.createTestingModule({
        providers: [
            TransportService,
            { provide: getModelToken(TransportRoute.name), useValue: stubModel() },
            { provide: getModelToken(TransportAssignment.name), useValue: stubModel() },
            { provide: getModelToken(Student.name), useValue: stubModel() },
            { provide: getModelToken(AcademicYear.name), useValue: stubModel() },
            { provide: AuditService, useValue: { log: jest.fn() } },
            {
                provide: SettingsService,
                useValue: {
                    getRazorpayCreds: jest.fn().mockImplementation(async () => credsFn()),
                },
            },
        ],
    }).compile();
    return mod.get(TransportService);
}

describe('TransportService — Razorpay credentials refresh (gap fix)', () => {
    it('uses creds from SettingsService when refreshed', async () => {
        const svc = await makeService({ keyId: 'rzp_test_T1', keySecret: 'secret_T1' });
        // Trigger refresh via private method
        await (svc as any).refreshRazorpayCreds();
        const razor = (svc as any).razor;
        expect(razor).toBeTruthy();
        expect(razor.__key_id).toBe('rzp_test_T1');
        expect(razor.__key_secret).toBe('secret_T1');
    });

    it('throws when credentials are not configured (empty)', async () => {
        const svc = await makeService({ keyId: '', keySecret: '' });
        await (svc as any).refreshRazorpayCreds();
        expect(() => (svc as any).getRazorpay()).toThrow(BadRequestException);
        expect(() => (svc as any).getRazorpay()).toThrow(/Razorpay keys in Settings/i);
    });

    it('rebuilds SDK when creds change between calls', async () => {
        let currentCreds = { keyId: 'rzp_v1', keySecret: 's_v1' };
        const svc = await makeService(() => currentCreds);
        await (svc as any).refreshRazorpayCreds();
        const razorV1 = (svc as any).razor;
        expect(razorV1.__key_id).toBe('rzp_v1');

        currentCreds = { keyId: 'rzp_v2', keySecret: 's_v2' };
        await (svc as any).refreshRazorpayCreds();
        const razorV2 = (svc as any).razor;
        expect(razorV2.__key_id).toBe('rzp_v2');
        expect(razorV2).not.toBe(razorV1); // New SDK instance
    });

    it('reuses SDK instance when creds unchanged (no GC churn)', async () => {
        const svc = await makeService({ keyId: 'rzp_x', keySecret: 'sec_x' });
        await (svc as any).refreshRazorpayCreds();
        const razorBefore = (svc as any).razor;
        await (svc as any).refreshRazorpayCreds(); // Same creds
        const razorAfter = (svc as any).razor;
        expect(razorAfter).toBe(razorBefore); // Same instance
    });

    it('handles transition from configured to cleared correctly', async () => {
        let currentCreds = { keyId: 'rzp_x', keySecret: 'sec_x' };
        const svc = await makeService(() => currentCreds);
        await (svc as any).refreshRazorpayCreds();
        expect((svc as any).razor).toBeTruthy();

        // Admin clears creds in UI
        currentCreds = { keyId: '', keySecret: '' };
        await (svc as any).refreshRazorpayCreds();
        expect((svc as any).razor).toBeNull();
        expect(() => (svc as any).getRazorpay()).toThrow(BadRequestException);
    });
});
