/**
 * test/services/FirebaseService.test.js
 *
 * Suite unitaria para api/services/FirebaseService.js
 *
 * ── Cobertura ──────────────────────────────────────────────────────────────
 *
 *  sendPushNotification
 *    ✓ ruta notification  (info == null)
 *    ✓ ruta data          (info != null)
 *    ✓ fallo no-crítico   → sails.log.warn, NO propaga excepción
 *    ✓ mensaje construido correctamente (token, title, body, data.data)
 *
 *  sendMultiPushNotification
 *    ✓ ruta notification  (info == null)
 *    ✓ ruta data          (info != null)
 *    ✓ tokens vacíos      → no llama a sendEachForMulticast
 *    ✓ fallo no-crítico   → sails.log.warn, NO propaga
 *    ✓ tokens fallidos    → sails.log.warn con lista de errores
 *
 *  Token cleanup (isStaleTokenError + clearStaleDeviceTokens)
 *    ✓ "Requested entity was not found" → token limpiado en BD
 *    ✓ "NotRegistered"                  → token limpiado en BD
 *    ✓ "not a valid FCM registration token" → token limpiado
 *    ✓ error.code messaging/registration-token-not-registered → limpiado
 *    ✓ error transitorio (network-error) → token NO limpiado
 *    ✓ solo los tokens inválidos se limpian, no los que sí enviaron
 *    ✓ limpieza en UserInfo, Useradmin y Foodtruck (3 modelos)
 *
 *  firebaseGetRestaurantOrders
 *    ✓ retorna snapshot.val()
 *    ✓ nodo inexistente   → retorna null
 *
 *  firebaseDeleteOrder
 *    ✓ borra nodos CurrentOrders y PendingOrders en restaurante
 *    ✓ borra nodos CurrentOrders y PendingOrders en rider
 *    ✓ borra tracking_status
 *    ✓ respuesta null en restaurante no rompe el flujo
 *    ✓ respuesta null en rider no rompe el flujo
 *    ✓ CurrentOrders/PendingOrders undefined no rompe el flujo
 *    ✓ solo borra el nodo cuyo order_id coincide (no borra otros)
 *
 *  firebaseUpdateTrackingStatus
 *    ✓ llama a db.ref(tracking_status/<id>).update con los campos correctos
 *    ✓ coerce a String (map_change y order_status undefined → '')
 *
 *  firebaseUpdateOrderPrice
 *    ✓ actualiza el nodo correcto en RiderOrderList (sin 's') / PendingOrders
 *    ✓ respuesta null en rider no lanza excepción
 *    ✓ PendingOrders undefined no lanza excepción
 *    ✓ DOCUMENTACIÓN: ruta de escritura usa RiderOrderList (sin 's'),
 *                     ruta de lectura usa RiderOrdersList (con 's') — inconsistencia conocida
 *
 *  firebaseMoveCurrentToPendingOrder
 *    ✓ borra de CurrentOrders y PendingOrders existentes
 *    ✓ crea nodo PendingOrder
 *    ✓ respuesta null en restaurante no lanza excepción
 *    ✓ CurrentOrders/PendingOrders undefined no lanza excepción
 *
 * ── Performance ────────────────────────────────────────────────────────────
 *    ✓ firebaseDeleteOrder: documenta oportunidad de paralelización
 *      (get restaurante + get rider son independientes — actualmente secuenciales)
 *    ✓ sendMultiPushNotification: fire-and-forget intencional documentado
 *
 * ── Regresión vs API anterior (https/request) ──────────────────────────────
 *    ✓ todas las funciones retornan Promises reales (await funciona)
 *    ✓ las mutaciones (update/remove/push) se awaitan antes de retornar
 */

'use strict';

const path = require('path');

// ─── Fixtures ────────────────────────────────────────────────────────────────

const RESTAURANT_ID  = 'rest-001';
const RIDER_ID       = 'rider-007';
const ORDER_ID       = 'order-123';

const FIREBASE_RESTAURANT_DATA = {
    CurrentOrders: {
        'ck01': { order_id: ORDER_ID, deal: '0', status: '2' },
        'ck02': { order_id: 'order-999', deal: '0', status: '1' },
    },
    PendingOrders: {
        'pk01': { order_id: ORDER_ID, deal: '0', status: '1' },
        'pk02': { order_id: 'order-888', deal: '0', status: '1' },
    },
};

const FIREBASE_RIDER_DATA = {
    CurrentOrders: {
        'rck01': { order_id: ORDER_ID, restaurants: 'R1', price: '100', status: '2' },
    },
    PendingOrders: {
        'rpk01': { order_id: ORDER_ID, restaurants: 'R1', price: '100', status: '1' },
        'rpk02': { order_id: 'order-777', restaurants: 'R2', price: '50', status: '1' },
    },
};

// ─── Helpers para construir el mock de Admin SDK ──────────────────────────────

/**
 * Construye un ref-stub que espía update/remove/push/once.
 * `once` retorna snapshot con `.val()` → snapshotValue.
 */
function makeRefMock(snapshotValue = null) {
    const ref = {
        update: jest.fn().mockResolvedValue(undefined),
        remove: jest.fn().mockResolvedValue(undefined),
        push  : jest.fn().mockResolvedValue({ key: 'new-key' }),
        once  : jest.fn().mockResolvedValue({ val: () => snapshotValue }),
    };
    return ref;
}

/**
 * db-stub cuyo `.ref(path)` devuelve siempre el mismo refMock
 * (suficiente para la mayoría de los tests).
 */
function makeDbMock(snapshotValue = null) {
    const ref = makeRefMock(snapshotValue);
    const db  = { ref: jest.fn().mockReturnValue(ref) };
    return { db, ref };
}

// ─── Mocks de módulos ─────────────────────────────────────────────────────────

// Debemos mockear ANTES de hacer require() al servicio.
// Jest.mock se eleva (hoisted) al inicio del archivo automáticamente.

let mockSend;
let mockSendEachForMulticast;
let mockDbRef;
let mockRef;

jest.mock('firebase-admin', () => {
    const initializeApp = jest.fn();
    const database      = jest.fn();   // se configura en beforeEach
    const credential    = { cert: jest.fn().mockReturnValue({}) };
    return { initializeApp, database, credential };
});

jest.mock('firebase-admin/messaging', () => {
    const getMessaging = jest.fn();    // se configura en beforeEach
    return { getMessaging };
});

// ─── Suite ───────────────────────────────────────────────────────────────────

describe('FirebaseService', () => {

    let svc;
    let adminMock;
    let getMessagingMock;

    /**
     * Recarga el módulo en cada test para que los mocks de
     * firebase-admin/messaging sean frescos.
     */
    beforeEach(() => {
        jest.resetModules();

        // Rebuild spies
        mockSend               = jest.fn().mockResolvedValue('msg-id-1');
        mockSendEachForMulticast = jest.fn().mockResolvedValue({
            failureCount: 0,
            responses: [],
        });
        mockRef    = makeRefMock();
        mockDbRef  = jest.fn().mockReturnValue(mockRef);

        // Wire firebase-admin mock
        adminMock = require('firebase-admin');
        adminMock.database.mockReturnValue({ ref: mockDbRef });

        // Wire firebase-admin/messaging mock
        getMessagingMock = require('firebase-admin/messaging');
        getMessagingMock.getMessaging.mockReturnValue({
            send: mockSend,
            sendEachForMulticast: mockSendEachForMulticast,
        });

        // Sails global con log mockeado
        global.sails = {
            log: {
                verbose: jest.fn(),
                warn   : jest.fn(),
                info   : jest.fn(),
                error  : jest.fn(),
            },
        };

        // Mocks de los modelos Waterline que clearStaleDeviceTokens necesita
        const makeModelUpdateMock = () => {
            const setMock = jest.fn().mockResolvedValue([]);
            const updateMock = jest.fn().mockReturnValue({ set: setMock });
            updateMock._set = setMock; // acceso directo en los tests
            return updateMock;
        };

        global.UserInfo  = { update: makeModelUpdateMock() };
        global.Useradmin = { update: makeModelUpdateMock() };
        global.Foodtruck = { update: makeModelUpdateMock() };

        svc = require(path.resolve(__dirname, '../../api/services/FirebaseService.js'));
    });

    afterEach(() => {
        delete global.sails;
        delete global.UserInfo;
        delete global.Useradmin;
        delete global.Foodtruck;
        jest.restoreAllMocks();
    });

    // =========================================================================
    // sendPushNotification
    // =========================================================================

    describe('sendPushNotification', () => {

        it('llama a getMessaging().send() con un mensaje tipo notification cuando info es null', async () => {
            const params = { token: 'tok-1', title: 'Hola', body: 'Mundo', info: null };

            await svc.sendPushNotification(params);

            expect(mockSend).toHaveBeenCalledTimes(1);
            const msg = mockSend.mock.calls[0][0];

            expect(msg.token).toBe('tok-1');
            expect(msg.notification).toBeDefined();
            expect(msg.notification.title).toBe('Hola');
            expect(msg.notification.body).toBe('Mundo');
            expect(msg).not.toHaveProperty('data');
        });

        it('llama a getMessaging().send() con un mensaje tipo data cuando info no es null', async () => {
            const info   = { orderId: 42, status: 'pending' };
            const params = { token: 'tok-2', title: 'Orden', body: 'Nueva', info };

            await svc.sendPushNotification(params);

            expect(mockSend).toHaveBeenCalledTimes(1);
            const msg = mockSend.mock.calls[0][0];

            expect(msg.token).toBe('tok-2');
            expect(msg.data).toBeDefined();
            expect(msg.data.title).toBe('Orden');
            expect(msg.data.body).toBe('Nueva');
            expect(msg.data.data).toBe(JSON.stringify(info));
            expect(msg).not.toHaveProperty('notification');
        });

        it('data.data es el JSON.stringify del objeto info', async () => {
            const info   = { a: 1, b: [2, 3] };
            const params = { token: 't', title: 'T', body: 'B', info };

            await svc.sendPushNotification(params);

            const msg = mockSend.mock.calls[0][0];
            expect(msg.data.data).toBe('{"a":1,"b":[2,3]}');
        });

        it('un fallo en send() NO propaga la excepción (error no-crítico)', async () => {
            mockSend.mockRejectedValueOnce(new Error('invalid-registration-token'));

            // No debe lanzar
            await expect(
                svc.sendPushNotification({ token: 't', title: 'T', body: 'B', info: null })
            ).resolves.toBeUndefined();
        });

        it('un fallo en send() registra sails.log.warn con el mensaje de error', async () => {
            mockSend.mockRejectedValueOnce(new Error('messaging/invalid-token'));

            await svc.sendPushNotification({ token: 't', title: 'T', body: 'B', info: null });

            expect(global.sails.log.warn).toHaveBeenCalledTimes(1);
            expect(global.sails.log.warn.mock.calls[0][0]).toMatch(/sendPushNotification/);
        });

        it('retorna una Promise real (await no rompe la cadena)', async () => {
            const result = await svc.sendPushNotification({
                token: 't', title: 'T', body: 'B', info: null
            });
            // sendPushNotification no devuelve valor útil — undefined es correcto
            expect(result).toBeUndefined();
        });
    });

    // =========================================================================
    // sendMultiPushNotification
    // =========================================================================

    describe('sendMultiPushNotification', () => {

        it('NO llama a sendEachForMulticast si tokens está vacío', async () => {
            await svc.sendMultiPushNotification({ tokens: [], title: 'T', body: 'B', info: null });
            expect(mockSendEachForMulticast).not.toHaveBeenCalled();
        });

        it('llama a sendEachForMulticast con mensaje tipo notification (info null)', async () => {
            const tokens = ['tok-a', 'tok-b'];
            await svc.sendMultiPushNotification({ tokens, title: 'T', body: 'B', info: null });

            expect(mockSendEachForMulticast).toHaveBeenCalledTimes(1);
            const msg = mockSendEachForMulticast.mock.calls[0][0];

            expect(msg.tokens).toEqual(tokens);
            expect(msg.notification).toBeDefined();
            expect(msg).not.toHaveProperty('data');
        });

        it('llama a sendEachForMulticast con mensaje tipo data (info no null)', async () => {
            const info   = { orderId: 7 };
            const tokens = ['tok-a'];
            await svc.sendMultiPushNotification({ tokens, title: 'T', body: 'B', info });

            const msg = mockSendEachForMulticast.mock.calls[0][0];
            expect(msg.data).toBeDefined();
            expect(msg.data.data).toBe(JSON.stringify(info));
        });

        it('un fallo en sendEachForMulticast registra sails.log.warn pero NO propaga', async () => {
            mockSendEachForMulticast.mockRejectedValueOnce(new Error('network-error'));

            await expect(
                svc.sendMultiPushNotification({ tokens: ['t'], title: 'T', body: 'B', info: null })
            ).resolves.toBeUndefined();

            // Esperar a que el .catch interno se ejecute (fire-and-forget interno)
            await new Promise(r => setImmediate(r));

            expect(global.sails.log.warn).toHaveBeenCalledTimes(1);
            expect(global.sails.log.warn.mock.calls[0][0]).toMatch(/sendMultiPushNotification/);
        });

        it('cuando hay tokens fallidos registra sails.log.warn con la lista de errores', async () => {
            mockSendEachForMulticast.mockResolvedValueOnce({
                failureCount: 1,
                responses: [
                    { success: true },
                    { success: false, error: new Error('unregistered') },
                ],
            });

            await svc.sendMultiPushNotification({
                tokens: ['tok-ok', 'tok-fail'], title: 'T', body: 'B', info: null
            });
            // Esperar al .then interno (fire-and-forget)
            await new Promise(r => setImmediate(r));

            expect(global.sails.log.warn).toHaveBeenCalledTimes(1);
            const warnArgs = global.sails.log.warn.mock.calls[0];
            expect(warnArgs[0]).toMatch(/tokens fallidos/);
            // El segundo argumento debe incluir el array de errores
            expect(Array.isArray(warnArgs[1])).toBe(true);
            expect(warnArgs[1]).toHaveLength(1);
        });

        /**
         * NOTA DE PERFORMANCE: sendMultiPushNotification es fire-and-forget por diseño
         * (sin await antes de getMessaging().sendEachForMulticast).
         * Esto es intencional para no bloquear el flujo del controlador,
         * pero significa que los errores solo se capturan vía .catch(), no vía try/catch
         * externo. Está documentado aquí para dejar constancia.
         */
        it('[PERF] sendEachForMulticast es fire-and-forget — el caller no espera el resultado', async () => {
            let resolved = false;
            mockSendEachForMulticast.mockImplementationOnce(
                () => new Promise(r => setTimeout(() => { resolved = true; r({ failureCount: 0, responses: [] }); }, 50))
            );

            await svc.sendMultiPushNotification({
                tokens: ['t'], title: 'T', body: 'B', info: null
            });

            // La función retornó ANTES de que sendEachForMulticast resolviera
            expect(resolved).toBe(false);
        });
    });

    // =========================================================================
    // firebaseGetRestaurantOrders
    // =========================================================================

    describe('firebaseGetRestaurantOrders', () => {

        it('lee el nodo restaurant/<id> y retorna snapshot.val()', async () => {
            mockRef.once.mockResolvedValueOnce({ val: () => FIREBASE_RESTAURANT_DATA });

            await svc.firebaseGetRestaurantOrders({ restaurant_id: RESTAURANT_ID });

            expect(mockDbRef).toHaveBeenCalledWith(`restaurant/${RESTAURANT_ID}`);
            expect(mockRef.once).toHaveBeenCalledWith('value');
        });

        it('retorna null cuando el nodo no existe', async () => {
            mockRef.once.mockResolvedValueOnce({ val: () => null });

            // firebaseGetRestaurantOrders es un wrapper — no retorna valor directamente,
            // pero no debe lanzar cuando el nodo es null
            await expect(
                svc.firebaseGetRestaurantOrders({ restaurant_id: RESTAURANT_ID })
            ).resolves.toBeUndefined();
        });
    });

    // =========================================================================
    // firebaseDeleteOrder
    // =========================================================================

    describe('firebaseDeleteOrder', () => {

        /**
         * Construye un db mock que retorna datos diferentes según la ruta:
         *   restaurant/<id>   → FIREBASE_RESTAURANT_DATA
         *   RiderOrdersList/<id> → FIREBASE_RIDER_DATA
         */
        function makeDeleteOrderDbMock() {
            const refMap = {
                [`restaurant/${RESTAURANT_ID}`]: makeRefMock(FIREBASE_RESTAURANT_DATA),
                [`RiderOrdersList/${RIDER_ID}`] : makeRefMock(FIREBASE_RIDER_DATA),
            };
            const defaultRef = makeRefMock(null);

            const db = {
                ref: jest.fn().mockImplementation((path) => refMap[path] || defaultRef),
            };
            return { db, refMap, defaultRef };
        }

        beforeEach(() => {
            const { db } = makeDeleteOrderDbMock();
            adminMock.database.mockReturnValue(db);
            // Re-require para que el módulo use el nuevo db mock
            jest.resetModules();
            adminMock = require('firebase-admin');
            adminMock.database.mockReturnValue(db);
            getMessagingMock = require('firebase-admin/messaging');
            getMessagingMock.getMessaging.mockReturnValue({
                send: mockSend,
                sendEachForMulticast: mockSendEachForMulticast,
            });
            svc = require(path.resolve(__dirname, '../../api/services/FirebaseService.js'));
        });

        it('llama a remove() para el nodo CurrentOrders cuyo order_id coincide en restaurante', async () => {
            await svc.firebaseDeleteOrder({
                rider_user_id: RIDER_ID,
                restaurant_id: RESTAURANT_ID,
                order_id: ORDER_ID,
            });

            const db = adminMock.database();
            // ck01 tiene order_id === ORDER_ID → debe borrarse
            expect(db.ref).toHaveBeenCalledWith(
                `restaurant/${RESTAURANT_ID}/CurrentOrders/ck01`
            );
            const ck01Ref = db.ref(`restaurant/${RESTAURANT_ID}/CurrentOrders/ck01`);
            expect(ck01Ref.remove).toHaveBeenCalled();
        });

        it('NO borra el nodo cuyo order_id NO coincide en restaurante', async () => {
            await svc.firebaseDeleteOrder({
                rider_user_id: RIDER_ID,
                restaurant_id: RESTAURANT_ID,
                order_id: ORDER_ID,
            });

            const db = adminMock.database();
            // ck02 tiene order_id === 'order-999' (diferente) — su ruta no debe llamarse
            expect(db.ref).not.toHaveBeenCalledWith(
                `restaurant/${RESTAURANT_ID}/CurrentOrders/ck02`
            );
        });

        it('borra el nodo PendingOrders del restaurante cuyo order_id coincide', async () => {
            await svc.firebaseDeleteOrder({
                rider_user_id: RIDER_ID,
                restaurant_id: RESTAURANT_ID,
                order_id: ORDER_ID,
            });

            const db = adminMock.database();
            expect(db.ref).toHaveBeenCalledWith(
                `restaurant/${RESTAURANT_ID}/PendingOrders/pk01`
            );
        });

        it('borra el nodo CurrentOrders del rider cuyo order_id coincide', async () => {
            await svc.firebaseDeleteOrder({
                rider_user_id: RIDER_ID,
                restaurant_id: RESTAURANT_ID,
                order_id: ORDER_ID,
            });

            const db = adminMock.database();
            expect(db.ref).toHaveBeenCalledWith(
                `RiderOrdersList/${RIDER_ID}/CurrentOrders/rck01`
            );
        });

        it('borra el nodo PendingOrders del rider cuyo order_id coincide', async () => {
            await svc.firebaseDeleteOrder({
                rider_user_id: RIDER_ID,
                restaurant_id: RESTAURANT_ID,
                order_id: ORDER_ID,
            });

            const db = adminMock.database();
            expect(db.ref).toHaveBeenCalledWith(
                `RiderOrdersList/${RIDER_ID}/PendingOrders/rpk01`
            );
        });

        it('llama a remove() sobre tracking_status/<order_id>', async () => {
            await svc.firebaseDeleteOrder({
                rider_user_id: RIDER_ID,
                restaurant_id: RESTAURANT_ID,
                order_id: ORDER_ID,
            });

            const db = adminMock.database();
            expect(db.ref).toHaveBeenCalledWith(`tracking_status/${ORDER_ID}`);
        });

        it('snapshot null en restaurante no lanza excepción', async () => {
            // Restaurante sin datos en Firebase
            const db = {
                ref: jest.fn().mockImplementation((p) => {
                    if (p === `restaurant/${RESTAURANT_ID}`)
                        {return makeRefMock(null);}
                    if (p === `RiderOrdersList/${RIDER_ID}`)
                        {return makeRefMock(FIREBASE_RIDER_DATA);}
                    return makeRefMock(null);
                }),
            };
            adminMock.database.mockReturnValue(db);
            jest.resetModules();
            adminMock = require('firebase-admin');
            adminMock.database.mockReturnValue(db);
            getMessagingMock = require('firebase-admin/messaging');
            getMessagingMock.getMessaging.mockReturnValue({ send: mockSend, sendEachForMulticast: mockSendEachForMulticast });
            svc = require(path.resolve(__dirname, '../../api/services/FirebaseService.js'));

            await expect(
                svc.firebaseDeleteOrder({ rider_user_id: RIDER_ID, restaurant_id: RESTAURANT_ID, order_id: ORDER_ID })
            ).resolves.toBeUndefined();
        });

        it('snapshot null en rider no lanza excepción', async () => {
            const db = {
                ref: jest.fn().mockImplementation((p) => {
                    if (p === `restaurant/${RESTAURANT_ID}`)
                        {return makeRefMock(FIREBASE_RESTAURANT_DATA);}
                    if (p === `RiderOrdersList/${RIDER_ID}`)
                        {return makeRefMock(null);}
                    return makeRefMock(null);
                }),
            };
            adminMock.database.mockReturnValue(db);
            jest.resetModules();
            adminMock = require('firebase-admin');
            adminMock.database.mockReturnValue(db);
            getMessagingMock = require('firebase-admin/messaging');
            getMessagingMock.getMessaging.mockReturnValue({ send: mockSend, sendEachForMulticast: mockSendEachForMulticast });
            svc = require(path.resolve(__dirname, '../../api/services/FirebaseService.js'));

            await expect(
                svc.firebaseDeleteOrder({ rider_user_id: RIDER_ID, restaurant_id: RESTAURANT_ID, order_id: ORDER_ID })
            ).resolves.toBeUndefined();
        });

        it('CurrentOrders/PendingOrders undefined en restaurante no lanza excepción', async () => {
            const db = {
                ref: jest.fn().mockImplementation((p) => {
                    if (p === `restaurant/${RESTAURANT_ID}`)
                        {return makeRefMock({ /* sin CurrentOrders ni PendingOrders */ });}
                    return makeRefMock(null);
                }),
            };
            adminMock.database.mockReturnValue(db);
            jest.resetModules();
            adminMock = require('firebase-admin');
            adminMock.database.mockReturnValue(db);
            getMessagingMock = require('firebase-admin/messaging');
            getMessagingMock.getMessaging.mockReturnValue({ send: mockSend, sendEachForMulticast: mockSendEachForMulticast });
            svc = require(path.resolve(__dirname, '../../api/services/FirebaseService.js'));

            await expect(
                svc.firebaseDeleteOrder({ rider_user_id: RIDER_ID, restaurant_id: RESTAURANT_ID, order_id: ORDER_ID })
            ).resolves.toBeUndefined();
        });

        /**
         * NOTA DE PERFORMANCE: firebaseGetRestaurantOrders y firebaseGetRiderOrders
         * se ejecutan SECUENCIALMENTE en firebaseDeleteOrder.
         * Ambas lecturas son independientes → candidatas a Promise.all.
         *
         * Impacto estimado: si cada once() tarda ~50ms en Firebase,
         * la paralelización reduciría el tiempo de ~100ms a ~50ms.
         *
         * Oportunidad de mejora:
         *   const [restaurantData, riderData] = await Promise.all([
         *       firebaseGetRestaurantOrders(restaurant_id),
         *       firebaseGetRiderOrders(rider_user_id),
         *   ]);
         *   // seguido de Promise.all([...removes])
         */
        it('[PERF] documenta que las lecturas de restaurante y rider son secuenciales (oportunidad de paralelización)', async () => {
            const callOrder = [];
            const db = {
                ref: jest.fn().mockImplementation((p) => {
                    const ref = makeRefMock(null);
                    ref.once.mockImplementationOnce(() => {
                        callOrder.push(p);
                        return Promise.resolve({ val: () => null });
                    });
                    return ref;
                }),
            };
            adminMock.database.mockReturnValue(db);
            jest.resetModules();
            adminMock = require('firebase-admin');
            adminMock.database.mockReturnValue(db);
            getMessagingMock = require('firebase-admin/messaging');
            getMessagingMock.getMessaging.mockReturnValue({ send: mockSend, sendEachForMulticast: mockSendEachForMulticast });
            svc = require(path.resolve(__dirname, '../../api/services/FirebaseService.js'));

            await svc.firebaseDeleteOrder({ rider_user_id: RIDER_ID, restaurant_id: RESTAURANT_ID, order_id: ORDER_ID });

            // Se confirma el orden secuencial actual: primero restaurante, luego rider
            expect(callOrder[0]).toBe(`restaurant/${RESTAURANT_ID}`);
            expect(callOrder[1]).toBe(`RiderOrdersList/${RIDER_ID}`);
            // ↑ Si se paralelizan en el futuro, este test deberá actualizarse
        });
    });

    // =========================================================================
    // firebaseUpdateTrackingStatus
    // =========================================================================

    describe('firebaseUpdateTrackingStatus', () => {

        it('llama a db.ref(tracking_status/<id>).update() con los campos correctos', async () => {
            await svc.firebaseUpdateTrackingStatus({
                order_id: ORDER_ID, map_change: '1', message: 'En camino'
            });

            expect(mockDbRef).toHaveBeenCalledWith(`tracking_status/${ORDER_ID}`);
            expect(mockRef.update).toHaveBeenCalledWith({
                map_change  : '1',
                order_status: 'En camino',
            });
        });

        it('coerce a String — valores undefined se convierten en cadena vacía', async () => {
            await svc.firebaseUpdateTrackingStatus({
                order_id: ORDER_ID, map_change: undefined, message: undefined
            });

            expect(mockRef.update).toHaveBeenCalledWith({
                map_change  : '',
                order_status: '',
            });
        });

        it('coerce a String — valores numéricos se convierten correctamente', async () => {
            await svc.firebaseUpdateTrackingStatus({
                order_id: ORDER_ID, map_change: 5, message: 42
            });

            expect(mockRef.update).toHaveBeenCalledWith({
                map_change  : '5',
                order_status: '42',
            });
        });

        it('la operación se awaita — no es fire-and-forget', async () => {
            let updateResolved = false;
            mockRef.update.mockImplementationOnce(
                () => new Promise(r => setImmediate(() => { updateResolved = true; r(); }))
            );

            await svc.firebaseUpdateTrackingStatus({
                order_id: ORDER_ID, map_change: '1', message: 'ok'
            });

            expect(updateResolved).toBe(true);
        });
    });

    // =========================================================================
    // firebaseUpdateOrderPrice
    // =========================================================================

    describe('firebaseUpdateOrderPrice', () => {

        it('actualiza el nodo RiderOrderList/<id>/PendingOrders/<key>', async () => {
            mockRef.once.mockResolvedValueOnce({
                val: () => ({
                    PendingOrders: {
                        'pk-x': { order_id: ORDER_ID, price: '80', restaurants: 'R1', status: '1' }
                    }
                })
            });

            await svc.firebaseUpdateOrderPrice({ rider_user_id: RIDER_ID, order_id: ORDER_ID });

            // Lectura: RiderOrdersList (con 's')
            expect(mockDbRef).toHaveBeenCalledWith(`RiderOrdersList/${RIDER_ID}`);

            // Escritura: RiderOrderList (sin 's') — inconsistencia conocida
            expect(mockDbRef).toHaveBeenCalledWith(
                `RiderOrderList/${RIDER_ID}/PendingOrders/pk-x`
            );
            expect(mockRef.update).toHaveBeenCalledWith(
                expect.objectContaining({
                    order_id: ORDER_ID,
                    price   : '80',
                    status  : '1',
                    symbol  : '$',
                })
            );
        });

        /**
         * ⚠ INCONSISTENCIA CONOCIDA:
         *   - firebaseGetRiderOrders lee de  RiderOrdersList/<id>  (con  's')
         *   - firebaseUpdateOrderPrice escribe en  RiderOrderList/<id>  (sin 's')
         *
         * Esta diferencia existía en el código original (antes de la migración
         * al Admin SDK) y se preservó tal cual para no alterar el comportamiento
         * en producción sin una verificación en Firebase.
         *
         * Acción recomendada: verificar en la consola de Firebase si el nodo
         * correcto es "RiderOrdersList" o "RiderOrderList" y unificar la ruta.
         */
        it('[BUG-KNOWN] la ruta de lectura usa RiderOrdersList (con s) y la de escritura usa RiderOrderList (sin s)', async () => {
            mockRef.once.mockResolvedValueOnce({
                val: () => ({
                    PendingOrders: {
                        'pk-y': { order_id: ORDER_ID, price: '50', restaurants: 'R2', status: '1' }
                    }
                })
            });

            await svc.firebaseUpdateOrderPrice({ rider_user_id: RIDER_ID, order_id: ORDER_ID });

            const allPaths = mockDbRef.mock.calls.map(([p]) => p);
            const readPath  = allPaths.find(p => p.startsWith('RiderOrdersList'));
            const writePath = allPaths.find(p => p.startsWith('RiderOrderList/') && p.includes('PendingOrders'));

            expect(readPath).toBe(`RiderOrdersList/${RIDER_ID}`);
            // La ruta de escritura usa el nodo SIN 's'
            expect(writePath).toMatch(new RegExp(`^RiderOrderList/${RIDER_ID}/PendingOrders/`));
            // Si en algún momento se unifican ambas rutas, este test fallará
            // como recordatorio para actualizar los datos de Firebase.
            expect(readPath).not.toBe(writePath.replace('/PendingOrders/pk-y', ''));
        });

        it('snapshot null en rider NO lanza excepción', async () => {
            mockRef.once.mockResolvedValueOnce({ val: () => null });

            await expect(
                svc.firebaseUpdateOrderPrice({ rider_user_id: RIDER_ID, order_id: ORDER_ID })
            ).resolves.toBeUndefined();

            expect(mockRef.update).not.toHaveBeenCalled();
        });

        it('PendingOrders undefined no lanza excepción', async () => {
            mockRef.once.mockResolvedValueOnce({ val: () => ({ /* sin PendingOrders */ }) });

            await expect(
                svc.firebaseUpdateOrderPrice({ rider_user_id: RIDER_ID, order_id: ORDER_ID })
            ).resolves.toBeUndefined();

            expect(mockRef.update).not.toHaveBeenCalled();
        });

        it('solo actualiza el nodo cuyo order_id coincide (no toca otros)', async () => {
            mockRef.once.mockResolvedValueOnce({
                val: () => ({
                    PendingOrders: {
                        'pk-match': { order_id: ORDER_ID,    price: '100', restaurants: 'R1', status: '1' },
                        'pk-other': { order_id: 'order-999', price: '200', restaurants: 'R2', status: '1' },
                    }
                })
            });

            await svc.firebaseUpdateOrderPrice({ rider_user_id: RIDER_ID, order_id: ORDER_ID });

            const writePaths = mockDbRef.mock.calls
                .map(([p]) => p)
                .filter(p => p.includes('PendingOrders'));

            expect(writePaths).toHaveLength(1);
            expect(writePaths[0]).toContain('pk-match');
            expect(writePaths[0]).not.toContain('pk-other');
        });
    });

    // =========================================================================
    // firebaseMoveCurrentToPendingOrder
    // =========================================================================

    describe('firebaseMoveCurrentToPendingOrder', () => {

        it('borra el nodo CurrentOrders cuyo order_id coincide en restaurante', async () => {
            mockRef.once.mockResolvedValueOnce({ val: () => FIREBASE_RESTAURANT_DATA });

            await svc.firebaseMoveCurrentToPendingOrder({
                restaurant_id: RESTAURANT_ID,
                order_id: ORDER_ID,
            });

            expect(mockDbRef).toHaveBeenCalledWith(
                `restaurant/${RESTAURANT_ID}/CurrentOrders/ck01`
            );
            expect(mockRef.remove).toHaveBeenCalled();
        });

        it('borra el nodo PendingOrders cuyo order_id coincide en restaurante', async () => {
            mockRef.once.mockResolvedValueOnce({ val: () => FIREBASE_RESTAURANT_DATA });

            await svc.firebaseMoveCurrentToPendingOrder({
                restaurant_id: RESTAURANT_ID,
                order_id: ORDER_ID,
            });

            expect(mockDbRef).toHaveBeenCalledWith(
                `restaurant/${RESTAURANT_ID}/PendingOrders/pk01`
            );
        });

        it('crea un nodo PendingOrder con los campos correctos', async () => {
            mockRef.once.mockResolvedValueOnce({ val: () => null });

            await svc.firebaseMoveCurrentToPendingOrder({
                restaurant_id: RESTAURANT_ID,
                order_id: ORDER_ID,
            });

            expect(mockDbRef).toHaveBeenCalledWith(
                `restaurant/${RESTAURANT_ID}/PendingOrders`
            );
            expect(mockRef.push).toHaveBeenCalledWith({
                deal    : '0',
                order_id: String(ORDER_ID),
                status  : '1',
                type    : '0',
            });
        });

        it('el push siempre se ejecuta independientemente de los datos del snapshot', async () => {
            // Incluso con snapshot null, el push de PendingOrder debe ejecutarse
            mockRef.once.mockResolvedValueOnce({ val: () => null });

            await svc.firebaseMoveCurrentToPendingOrder({
                restaurant_id: RESTAURANT_ID,
                order_id: ORDER_ID,
            });

            expect(mockRef.push).toHaveBeenCalledTimes(1);
        });

        it('CurrentOrders y PendingOrders undefined no lanzan excepción', async () => {
            mockRef.once.mockResolvedValueOnce({
                val: () => ({ /* sin CurrentOrders ni PendingOrders */ })
            });

            await expect(
                svc.firebaseMoveCurrentToPendingOrder({
                    restaurant_id: RESTAURANT_ID,
                    order_id: ORDER_ID,
                })
            ).resolves.toBeUndefined();
        });

        it('la operación push se awaita antes de retornar', async () => {
            mockRef.once.mockResolvedValueOnce({ val: () => null });

            let pushResolved = false;
            mockRef.push.mockImplementationOnce(
                () => new Promise(r => setImmediate(() => { pushResolved = true; r({ key: 'k' }); }))
            );

            await svc.firebaseMoveCurrentToPendingOrder({
                restaurant_id: RESTAURANT_ID,
                order_id: ORDER_ID,
            });

            expect(pushResolved).toBe(true);
        });
    });

    // =========================================================================
    // Token cleanup — isStaleTokenError + clearStaleDeviceTokens
    // =========================================================================

    describe('Token cleanup (tokens FCM inválidos)', () => {

        /**
         * Helper: configura sendEachForMulticast para devolver una respuesta con
         * errores específicos para cada token.
         *
         * @param {Array<{token: string, errorCode?: string, errorMessage?: string}>} entries
         */
        function setupMulticastResponse(entries) {
            const responses = entries.map(({ errorCode, errorMessage }) => {
                if (!errorCode && !errorMessage) {
                    return { success: true };
                }
                const error = new Error(errorMessage || errorCode);
                if (errorCode) { error.code = errorCode; }
                return { success: false, error };
            });

            mockSendEachForMulticast.mockResolvedValueOnce({
                failureCount: responses.filter(r => !r.success).length,
                responses,
            });
        }

        it('token con "Requested entity was not found" se limpia en los 3 modelos', async () => {
            const staleToken = 'stale-tok-001';
            setupMulticastResponse([
                { token: 'ok-tok', },
                { token: staleToken, errorMessage: 'Requested entity was not found.' },
            ]);

            await svc.sendMultiPushNotification({
                tokens: ['ok-tok', staleToken], title: 'T', body: 'B', info: null
            });
            await new Promise(r => setImmediate(r));

            expect(global.UserInfo.update).toHaveBeenCalledWith({ device_token: [staleToken] });
            expect(global.Useradmin.update).toHaveBeenCalledWith({ device_token: [staleToken] });
            expect(global.Foodtruck.update).toHaveBeenCalledWith({ device_token: [staleToken] });
        });

        it('token con "NotRegistered" se limpia en los 3 modelos', async () => {
            const staleToken = 'stale-tok-002';
            setupMulticastResponse([
                { token: staleToken, errorMessage: 'NotRegistered' },
            ]);

            await svc.sendMultiPushNotification({
                tokens: [staleToken], title: 'T', body: 'B', info: null
            });
            await new Promise(r => setImmediate(r));

            expect(global.UserInfo.update).toHaveBeenCalledWith({ device_token: [staleToken] });
        });

        it('token con "not a valid FCM registration token" se limpia', async () => {
            const staleToken = 'invalid-format-tok';
            setupMulticastResponse([
                { token: staleToken, errorMessage: 'The registration token is not a valid FCM registration token' },
            ]);

            await svc.sendMultiPushNotification({
                tokens: [staleToken], title: 'T', body: 'B', info: null
            });
            await new Promise(r => setImmediate(r));

            expect(global.UserInfo.update).toHaveBeenCalledWith({ device_token: [staleToken] });
        });

        it('token con error.code messaging/registration-token-not-registered se limpia', async () => {
            const staleToken = 'stale-tok-003';
            setupMulticastResponse([
                { token: staleToken, errorCode: 'messaging/registration-token-not-registered' },
            ]);

            await svc.sendMultiPushNotification({
                tokens: [staleToken], title: 'T', body: 'B', info: null
            });
            await new Promise(r => setImmediate(r));

            expect(global.UserInfo.update).toHaveBeenCalledWith({ device_token: [staleToken] });
        });

        it('token con error.code messaging/invalid-registration-token se limpia', async () => {
            const staleToken = 'stale-tok-004';
            setupMulticastResponse([
                { token: staleToken, errorCode: 'messaging/invalid-registration-token' },
            ]);

            await svc.sendMultiPushNotification({
                tokens: [staleToken], title: 'T', body: 'B', info: null
            });
            await new Promise(r => setImmediate(r));

            expect(global.UserInfo.update).toHaveBeenCalledWith({ device_token: [staleToken] });
        });

        it('error transitorio (network-error) NO limpia el token', async () => {
            const validToken = 'valid-tok-net';
            setupMulticastResponse([
                { token: validToken, errorMessage: 'network-error: connection reset' },
            ]);

            await svc.sendMultiPushNotification({
                tokens: [validToken], title: 'T', body: 'B', info: null
            });
            await new Promise(r => setImmediate(r));

            expect(global.UserInfo.update).not.toHaveBeenCalled();
            expect(global.Useradmin.update).not.toHaveBeenCalled();
            expect(global.Foodtruck.update).not.toHaveBeenCalled();
        });

        it('error internal-error NO limpia el token (puede ser transitorio)', async () => {
            setupMulticastResponse([
                { token: 'tok-int', errorCode: 'messaging/internal-error' },
            ]);

            await svc.sendMultiPushNotification({
                tokens: ['tok-int'], title: 'T', body: 'B', info: null
            });
            await new Promise(r => setImmediate(r));

            expect(global.UserInfo.update).not.toHaveBeenCalled();
        });

        it('solo los tokens inválidos se incluyen en la limpieza (los válidos NO)', async () => {
            const goodToken  = 'tok-good';
            const staleToken = 'tok-stale';

            setupMulticastResponse([
                { token: goodToken },                                                         // éxito
                { token: staleToken, errorMessage: 'Requested entity was not found.' },       // inválido
            ]);

            await svc.sendMultiPushNotification({
                tokens: [goodToken, staleToken], title: 'T', body: 'B', info: null
            });
            await new Promise(r => setImmediate(r));

            // La llamada .update SOLO recibe el token inválido
            const calledWith = global.UserInfo.update.mock.calls[0][0];
            expect(calledWith).toEqual({ device_token: [staleToken] });
            expect(calledWith.device_token).not.toContain(goodToken);
        });

        it('múltiples tokens inválidos se pasan juntos en una sola llamada por modelo', async () => {
            const stale1 = 'stale-1';
            const stale2 = 'stale-2';
            const stale3 = 'stale-3';

            setupMulticastResponse([
                { token: stale1, errorMessage: 'NotRegistered' },
                { token: 'ok' },
                { token: stale2, errorCode: 'messaging/registration-token-not-registered' },
                { token: stale3, errorCode: 'messaging/invalid-registration-token' },
            ]);

            await svc.sendMultiPushNotification({
                tokens: [stale1, 'ok', stale2, stale3], title: 'T', body: 'B', info: null
            });
            await new Promise(r => setImmediate(r));

            // Una sola llamada con los 3 tokens juntos (no 3 llamadas separadas)
            expect(global.UserInfo.update).toHaveBeenCalledTimes(1);
            const calledWith = global.UserInfo.update.mock.calls[0][0];
            expect(calledWith.device_token).toHaveLength(3);
            expect(calledWith.device_token).toContain(stale1);
            expect(calledWith.device_token).toContain(stale2);
            expect(calledWith.device_token).toContain(stale3);
        });

        it('la limpieza llama a .set({ device_token: "" }) en los 3 modelos', async () => {
            const staleToken = 'tok-set-check';
            setupMulticastResponse([
                { token: staleToken, errorMessage: 'NotRegistered' },
            ]);

            await svc.sendMultiPushNotification({
                tokens: [staleToken], title: 'T', body: 'B', info: null
            });
            await new Promise(r => setImmediate(r));

            expect(global.UserInfo.update().__proto__).toBeDefined();
            // Verificamos que .set fue llamado con device_token vacío
            const setMockUserInfo  = global.UserInfo.update.mock.results[0].value.set;
            const setMockUseradmin = global.Useradmin.update.mock.results[0].value.set;
            const setMockFoodtruck = global.Foodtruck.update.mock.results[0].value.set;

            expect(setMockUserInfo).toHaveBeenCalledWith({ device_token: '' });
            expect(setMockUseradmin).toHaveBeenCalledWith({ device_token: '' });
            expect(setMockFoodtruck).toHaveBeenCalledWith({ device_token: '' });
        });

        it('si no hay tokens fallidos NO llama a model.update()', async () => {
            mockSendEachForMulticast.mockResolvedValueOnce({
                failureCount: 0,
                responses: [{ success: true }, { success: true }],
            });

            await svc.sendMultiPushNotification({
                tokens: ['t1', 't2'], title: 'T', body: 'B', info: null
            });
            await new Promise(r => setImmediate(r));

            expect(global.UserInfo.update).not.toHaveBeenCalled();
            expect(global.Useradmin.update).not.toHaveBeenCalled();
            expect(global.Foodtruck.update).not.toHaveBeenCalled();
        });

        it('un fallo en model.update() NO rompe el flujo (se loggea como warn)', async () => {
            const staleToken = 'tok-db-fail';
            setupMulticastResponse([
                { token: staleToken, errorMessage: 'NotRegistered' },
            ]);

            // Simular fallo en la BD
            global.UserInfo.update.mockReturnValueOnce({
                set: jest.fn().mockRejectedValueOnce(new Error('DB timeout'))
            });

            // No debe lanzar
            await expect(
                svc.sendMultiPushNotification({
                    tokens: [staleToken], title: 'T', body: 'B', info: null
                })
            ).resolves.toBeUndefined();

            await new Promise(r => setImmediate(r));

            expect(global.sails.log.warn).toHaveBeenCalledWith(
                expect.stringContaining('No se pudo limpiar tokens en UserInfo'),
                expect.any(String)
            );
        });

        it('registra sails.log.info con el conteo de tokens a limpiar', async () => {
            const staleToken = 'tok-log-check';
            setupMulticastResponse([
                { token: staleToken, errorMessage: 'NotRegistered' },
            ]);

            await svc.sendMultiPushNotification({
                tokens: [staleToken], title: 'T', body: 'B', info: null
            });
            await new Promise(r => setImmediate(r));

            expect(global.sails.log.info).toHaveBeenCalledWith(
                expect.stringContaining('Limpiando tokens inválidos'),
                1
            );
        });
    });

    // =========================================================================
    // Regresión: Admin SDK vs API antigua (https/request)
    // =========================================================================

    describe('[REGRESIÓN] Admin SDK devuelve Promises reales', () => {

        it('sendPushNotification retorna una Promise', () => {
            const result = svc.sendPushNotification({ token: 't', title: 'T', body: 'B', info: null });
            expect(result).toBeInstanceOf(Promise);
        });

        it('firebaseGetRestaurantOrders retorna una Promise', () => {
            const result = svc.firebaseGetRestaurantOrders({ restaurant_id: RESTAURANT_ID });
            expect(result).toBeInstanceOf(Promise);
        });

        it('firebaseUpdateTrackingStatus retorna una Promise', () => {
            const result = svc.firebaseUpdateTrackingStatus({
                order_id: ORDER_ID, map_change: '1', message: 'ok'
            });
            expect(result).toBeInstanceOf(Promise);
        });

        it('firebaseDeleteOrder retorna una Promise', () => {
            mockRef.once.mockResolvedValue({ val: () => null });
            const result = svc.firebaseDeleteOrder({
                rider_user_id: RIDER_ID, restaurant_id: RESTAURANT_ID, order_id: ORDER_ID
            });
            expect(result).toBeInstanceOf(Promise);
        });

        it('firebaseMoveCurrentToPendingOrder retorna una Promise', () => {
            mockRef.once.mockResolvedValue({ val: () => null });
            const result = svc.firebaseMoveCurrentToPendingOrder({
                restaurant_id: RESTAURANT_ID, order_id: ORDER_ID
            });
            expect(result).toBeInstanceOf(Promise);
        });

        /**
         * Con la API antigua (request + callback), las funciones async
         * resolvían inmediatamente sin esperar el resultado de Firebase.
         * Con el Admin SDK, las operaciones se awaitan correctamente.
         * Este test verifica que el comportamiento es el esperado.
         */
        it('firebaseUpdateTrackingStatus awaita el update antes de retornar', async () => {
            let updateSettled = false;
            mockRef.update.mockImplementationOnce(() =>
                new Promise(r => setImmediate(() => { updateSettled = true; r(); }))
            );

            await svc.firebaseUpdateTrackingStatus({
                order_id: ORDER_ID, map_change: '2', message: 'Delivered'
            });

            expect(updateSettled).toBe(true);
        });

        it('firebaseMoveCurrentToPendingOrder awaita el remove antes del push', async () => {
            mockRef.once.mockResolvedValueOnce({
                val: () => ({
                    CurrentOrders: { 'k1': { order_id: ORDER_ID } }
                })
            });

            const callOrder = [];
            mockRef.remove.mockImplementationOnce(
                () => new Promise(r => setImmediate(() => { callOrder.push('remove'); r(); }))
            );
            mockRef.push.mockImplementationOnce(
                () => new Promise(r => setImmediate(() => { callOrder.push('push'); r({ key: 'k' }); }))
            );

            await svc.firebaseMoveCurrentToPendingOrder({
                restaurant_id: RESTAURANT_ID,
                order_id: ORDER_ID,
            });

            expect(callOrder).toEqual(['remove', 'push']);
        });
    });
});
