/**
 * session.spec.js
 *
 * Cobre o módulo de sessão global — foco nas race conditions documentadas
 * no próprio session.js (singleflight, SIGNED_IN redundante, TOKEN_REFRESHED).
 *
 * Mock do supabase: capturamos o callback de onAuthStateChange pra disparar
 * eventos manualmente e observar o state dos refs reativos.
 */
import { describe, it, expect, vi, beforeEach } from 'vitest';

// Captura do callback de onAuthStateChange (setado no listenAuthChanges)
let authCallback = null;

// Mock configurável de getSession (pode trocar em cada teste via mockImplementation)
const getSessionMock = vi.fn();
const profileSingleMock = vi.fn();
const saasMaybeSingleMock = vi.fn();

vi.mock('@/lib/supabase/client', () => {
    const from = vi.fn((table) => {
        return {
            select: vi.fn().mockReturnThis(),
            eq: vi.fn().mockReturnThis(),
            maybeSingle: table === 'saas_admins' ? saasMaybeSingleMock : profileSingleMock,
            single: vi.fn().mockResolvedValue({ data: null, error: null })
        };
    });

    return {
        supabase: {
            auth: {
                getSession: getSessionMock,
                onAuthStateChange: vi.fn((cb) => {
                    authCallback = cb;
                    return { data: { subscription: { unsubscribe: vi.fn() } } };
                })
            },
            from
        }
    };
});

vi.mock('@/support/supportLogger', () => ({
    logAuth: vi.fn(),
    logError: vi.fn()
}));

// Importa depois dos mocks
const session = await import('../session.js');

beforeEach(() => {
    // Reseta state dos refs e mocks a cada teste
    session.sessionUser.value = null;
    session.sessionRole.value = null;
    session.sessionIsSaasAdmin.value = false;
    session.sessionReady.value = false;
    session.sessionRefreshing.value = false;

    // Desfaz listenAuthChanges de teste anterior pra permitir re-registro
    session.stopAuthChanges();

    authCallback = null;
    getSessionMock.mockReset();
    profileSingleMock.mockReset();
    saasMaybeSingleMock.mockReset();

    // defaults razoáveis
    profileSingleMock.mockResolvedValue({ data: { role: 'therapist' }, error: null });
    saasMaybeSingleMock.mockResolvedValue({ data: null, error: null });
});

describe('initSession — boot inicial', () => {
    it('sem sessão → zera user/role/saasAdmin', async () => {
        getSessionMock.mockResolvedValue({ data: { session: null }, error: null });

        await session.initSession({ initial: true });

        expect(session.sessionUser.value).toBe(null);
        expect(session.sessionRole.value).toBe(null);
        expect(session.sessionIsSaasAdmin.value).toBe(false);
        expect(session.sessionReady.value).toBe(true);
        expect(session.sessionRefreshing.value).toBe(false);
    });

    it('com sessão → hydrata user + busca role', async () => {
        getSessionMock.mockResolvedValue({
            data: { session: { user: { id: 'uid-1' } } },
            error: null
        });
        profileSingleMock.mockResolvedValue({ data: { role: 'clinic_admin' }, error: null });

        await session.initSession({ initial: true });

        expect(session.sessionUser.value?.id).toBe('uid-1');
        expect(session.sessionRole.value).toBe('clinic_admin');
        expect(session.sessionReady.value).toBe(true);
    });

    it('erro em getSession → state zerado (não propaga)', async () => {
        getSessionMock.mockRejectedValue(new Error('network down'));

        await session.initSession({ initial: true });

        expect(session.sessionUser.value).toBe(null);
        expect(session.sessionRole.value).toBe(null);
        expect(session.sessionReady.value).toBe(true); // ainda marca ready pra não travar o guard
    });

    it('singleflight: 2 chamadas concorrentes fazem apenas 1 getSession', async () => {
        let resolveGet;
        getSessionMock.mockImplementation(() => new Promise((resolve) => { resolveGet = resolve; }));

        const p1 = session.initSession({ initial: true });
        const p2 = session.initSession({ initial: true });

        // segunda chamada deve ter retornado a mesma promise (sem disparar getSession de novo)
        expect(getSessionMock).toHaveBeenCalledTimes(1);

        resolveGet({ data: { session: null }, error: null });
        await Promise.all([p1, p2]);
    });
});

describe('refreshSession — evita corrida', () => {
    it('não dispara se já está refreshing', async () => {
        session.sessionRefreshing.value = true;

        await session.refreshSession();

        expect(getSessionMock).not.toHaveBeenCalled();
    });

    it('sem sessão → não zera state existente (SIGNED_OUT cuida)', async () => {
        session.sessionUser.value = { id: 'uid-1' };
        session.sessionRole.value = 'therapist';
        getSessionMock.mockResolvedValue({ data: { session: null }, error: null });

        await session.refreshSession();

        // State preservado — refreshSession não é quem zera (é SIGNED_OUT)
        expect(session.sessionUser.value?.id).toBe('uid-1');
        expect(session.sessionRole.value).toBe('therapist');
    });

    it('mesma sessão consistente → no-op', async () => {
        session.sessionUser.value = { id: 'uid-1' };
        session.sessionRole.value = 'therapist';
        getSessionMock.mockResolvedValue({
            data: { session: { user: { id: 'uid-1' } } },
            error: null
        });

        await session.refreshSession();

        // initSession não foi chamado de novo (state já era consistente)
        expect(getSessionMock).toHaveBeenCalledTimes(1); // só o refreshSession próprio
    });
});

describe('listenAuthChanges — callbacks de auth', () => {
    it('SIGNED_OUT zera state + chama callback', async () => {
        const onOut = vi.fn();
        session.setOnSignedOut(onOut);
        session.listenAuthChanges();

        // simula state previamente hydratado
        session.sessionUser.value = { id: 'uid-1' };
        session.sessionRole.value = 'therapist';
        session.sessionIsSaasAdmin.value = true;
        session.sessionRefreshing.value = true;

        expect(authCallback).toBeTypeOf('function');
        await authCallback('SIGNED_OUT', null);

        expect(session.sessionUser.value).toBe(null);
        expect(session.sessionRole.value).toBe(null);
        expect(session.sessionIsSaasAdmin.value).toBe(false);
        expect(session.sessionRefreshing.value).toBe(false);
        expect(session.sessionReady.value).toBe(true);
        expect(onOut).toHaveBeenCalledTimes(1);
    });

    it('SIGNED_IN com mesmo user (redundante) é ignorado', async () => {
        session.listenAuthChanges();
        session.sessionUser.value = { id: 'uid-1' };
        session.sessionRole.value = 'therapist';
        session.sessionReady.value = true;

        await authCallback('SIGNED_IN', { user: { id: 'uid-1' } });

        // Não rehidratou — profileSingleMock não foi chamado
        expect(profileSingleMock).not.toHaveBeenCalled();
    });

    it('SIGNED_IN com user diferente → hydrata novo', async () => {
        session.listenAuthChanges();
        session.sessionUser.value = { id: 'uid-1' };
        session.sessionRole.value = 'therapist';
        session.sessionReady.value = true;

        profileSingleMock.mockResolvedValue({ data: { role: 'clinic_admin' }, error: null });

        await authCallback('SIGNED_IN', { user: { id: 'uid-2' } });

        expect(session.sessionUser.value?.id).toBe('uid-2');
        expect(session.sessionRole.value).toBe('clinic_admin');
    });
});

describe('stopAuthChanges — cleanup', () => {
    it('unsubscribe é chamado', () => {
        session.listenAuthChanges();
        session.stopAuthChanges();

        // não deve lançar erro se chamar de novo
        expect(() => session.stopAuthChanges()).not.toThrow();
    });
});
