/**
 * tenantStore.spec.js — T#5
 *
 * Cobre os caminhos críticos do tenantStore:
 *  - ensureLoaded singleflight (não dispara load duplicado)
 *  - loadSessionAndTenant: usuário não logado, com memberships, com saved
 *    tenant válido/inválido, fallback ao primeiro active
 *  - setActiveTenant: tenant válido vs inválido (regressão V#5: tenant_id
 *    de outro usuário não pode ser herdado)
 *  - reset: limpa state e localStorage
 *  - getters: tenantId / currentTenantId / tenant / hasActiveTenant
 */
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { setActivePinia, createPinia } from 'pinia';

// Stub mínimo de localStorage (vitest roda em env "node" sem jsdom).
// Mantém Map em memória, mesmo contrato síncrono.
const lsBacking = new Map();
globalThis.localStorage = {
    getItem: (k) => (lsBacking.has(k) ? lsBacking.get(k) : null),
    setItem: (k, v) => lsBacking.set(k, String(v)),
    removeItem: (k) => lsBacking.delete(k),
    clear: () => lsBacking.clear(),
    key: (i) => Array.from(lsBacking.keys())[i] ?? null,
    get length() {
        return lsBacking.size;
    }
};

const getSessionMock = vi.fn();
const rpcMock = vi.fn();

vi.mock('@/lib/supabase/client', () => ({
    supabase: {
        auth: { getSession: (...a) => getSessionMock(...a) },
        rpc: (...a) => rpcMock(...a)
    }
}));

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

const { useTenantStore } = await import('../tenantStore.js');

beforeEach(() => {
    setActivePinia(createPinia());
    getSessionMock.mockReset();
    rpcMock.mockReset();
    localStorage.clear();
});

const USER = { id: 'u-1', email: 'user@test.com' };
const SESSION = { user: USER };

const memActive = (tenantId, role = 'tenant_admin', kind = 'clinic') => ({
    tenant_id: tenantId,
    role,
    kind,
    status: 'active'
});

describe('loadSessionAndTenant — usuário não autenticado', () => {
    it('limpa estado quando não há sessão', async () => {
        getSessionMock.mockResolvedValue({ data: { session: null }, error: null });

        const s = useTenantStore();
        await s.loadSessionAndTenant();

        expect(s.loaded).toBe(true);
        expect(s.user).toBe(null);
        expect(s.activeTenantId).toBe(null);
        expect(s.activeRole).toBe(null);
        expect(s.memberships).toEqual([]);
        expect(s.needsTenantLink).toBe(false);
        // não chamou my_tenants sem user
        expect(rpcMock).not.toHaveBeenCalled();
    });
});

describe('loadSessionAndTenant — usuário com memberships', () => {
    it('seleciona primeiro active quando não há saved', async () => {
        getSessionMock.mockResolvedValue({ data: { session: SESSION }, error: null });
        rpcMock.mockResolvedValue({ data: [memActive('t1'), memActive('t2')], error: null });

        const s = useTenantStore();
        await s.loadSessionAndTenant();

        expect(s.activeTenantId).toBe('t1');
        expect(s.activeRole).toBe('clinic_admin');
        expect(localStorage.getItem('tenant_id')).toBe('t1');
        expect(s.needsTenantLink).toBe(false);
    });

    it('respeita saved tenant se pertencer ao usuário (membership active)', async () => {
        localStorage.setItem('tenant_id', 't2');
        localStorage.setItem('tenant', JSON.stringify({ id: 't2', role: 'clinic_admin' }));

        getSessionMock.mockResolvedValue({ data: { session: SESSION }, error: null });
        rpcMock.mockResolvedValue({ data: [memActive('t1'), memActive('t2')], error: null });

        const s = useTenantStore();
        await s.loadSessionAndTenant();

        expect(s.activeTenantId).toBe('t2');
    });

    it('regressão V#5: descarta saved tenant que NÃO pertence ao usuário e cai no primeiro active', async () => {
        localStorage.setItem('tenant_id', 't-foreign');
        localStorage.setItem('tenant', JSON.stringify({ id: 't-foreign', role: 'clinic_admin' }));

        getSessionMock.mockResolvedValue({ data: { session: SESSION }, error: null });
        rpcMock.mockResolvedValue({ data: [memActive('t1')], error: null });

        const s = useTenantStore();
        await s.loadSessionAndTenant();

        expect(s.activeTenantId).toBe('t1');
        // localStorage foi atualizado pro tenant correto
        expect(localStorage.getItem('tenant_id')).toBe('t1');
    });

    it('needsTenantLink=true quando usuário não tem nenhum membership active', async () => {
        getSessionMock.mockResolvedValue({ data: { session: SESSION }, error: null });
        rpcMock.mockResolvedValue({ data: [{ ...memActive('t1'), status: 'pending' }], error: null });

        const s = useTenantStore();
        await s.loadSessionAndTenant();

        expect(s.activeTenantId).toBe(null);
        expect(s.needsTenantLink).toBe(true);
    });

    it('normaliza role via roleNormalizer (tenant_admin + kind=therapist → therapist)', async () => {
        getSessionMock.mockResolvedValue({ data: { session: SESSION }, error: null });
        rpcMock.mockResolvedValue({ data: [memActive('t1', 'tenant_admin', 'therapist')], error: null });

        const s = useTenantStore();
        await s.loadSessionAndTenant();

        expect(s.activeRole).toBe('therapist');
    });
});

describe('ensureLoaded — singleflight', () => {
    it('não recarrega se já loaded', async () => {
        getSessionMock.mockResolvedValue({ data: { session: SESSION }, error: null });
        rpcMock.mockResolvedValue({ data: [memActive('t1')], error: null });

        const s = useTenantStore();
        await s.ensureLoaded();
        await s.ensureLoaded();
        await s.ensureLoaded();

        // só uma chamada de getSession
        expect(getSessionMock).toHaveBeenCalledTimes(1);
    });

    it('chamadas concorrentes compartilham a mesma promise', async () => {
        let resolveSession;
        const sessionPromise = new Promise((res) => {
            resolveSession = res;
        });
        getSessionMock.mockReturnValue(sessionPromise);
        rpcMock.mockResolvedValue({ data: [memActive('t1')], error: null });

        const s = useTenantStore();
        const p1 = s.ensureLoaded();
        const p2 = s.ensureLoaded();
        const p3 = s.ensureLoaded();

        // resolve a única getSession
        resolveSession({ data: { session: SESSION }, error: null });
        await Promise.all([p1, p2, p3]);

        expect(getSessionMock).toHaveBeenCalledTimes(1);
        expect(rpcMock).toHaveBeenCalledTimes(1);
    });
});

describe('loadSessionAndTenant — erros', () => {
    it('erro em getSession seta error e marca loaded', async () => {
        getSessionMock.mockResolvedValue({ data: null, error: new Error('session fail') });

        const s = useTenantStore();
        await s.loadSessionAndTenant();

        expect(s.error).toBeTruthy();
        expect(s.loaded).toBe(true);
    });

    it('erro em my_tenants seta error e mantém estado pré-existente', async () => {
        getSessionMock.mockResolvedValue({ data: { session: SESSION }, error: null });
        rpcMock.mockResolvedValue({ data: null, error: new Error('rpc fail') });

        const s = useTenantStore();
        await s.loadSessionAndTenant();

        expect(s.error).toBeTruthy();
        expect(s.user).toEqual(USER); // user já tinha sido setado antes do throw
        expect(s.loaded).toBe(true);
    });
});

describe('setActiveTenant — troca explícita', () => {
    it('aceita tenant válido (membership active)', async () => {
        getSessionMock.mockResolvedValue({ data: { session: SESSION }, error: null });
        rpcMock.mockResolvedValue({ data: [memActive('t1'), memActive('t2', 'tenant_admin', 'therapist')], error: null });

        const s = useTenantStore();
        await s.loadSessionAndTenant();
        s.setActiveTenant('t2');

        expect(s.activeTenantId).toBe('t2');
        expect(s.activeRole).toBe('therapist');
        expect(localStorage.getItem('tenant_id')).toBe('t2');
    });

    it('rejeita tenant que não está nos memberships (sem mudar nada → null)', async () => {
        getSessionMock.mockResolvedValue({ data: { session: SESSION }, error: null });
        rpcMock.mockResolvedValue({ data: [memActive('t1')], error: null });

        const s = useTenantStore();
        await s.loadSessionAndTenant();
        s.setActiveTenant('t-foreign');

        expect(s.activeTenantId).toBe(null);
        expect(s.activeRole).toBe(null);
        expect(s.needsTenantLink).toBe(true);
        expect(localStorage.getItem('tenant_id')).toBe(null);
    });
});

describe('reset — limpeza completa', () => {
    it('zera estado e localStorage', async () => {
        getSessionMock.mockResolvedValue({ data: { session: SESSION }, error: null });
        rpcMock.mockResolvedValue({ data: [memActive('t1')], error: null });

        const s = useTenantStore();
        await s.loadSessionAndTenant();
        s.reset();

        expect(s.user).toBe(null);
        expect(s.activeTenantId).toBe(null);
        expect(s.activeRole).toBe(null);
        expect(s.memberships).toEqual([]);
        expect(s.loaded).toBe(false);
        expect(localStorage.getItem('tenant_id')).toBe(null);
    });
});

describe('getters', () => {
    it('tenantId / currentTenantId / role expõem activeTenantId / activeRole', async () => {
        getSessionMock.mockResolvedValue({ data: { session: SESSION }, error: null });
        rpcMock.mockResolvedValue({ data: [memActive('t1')], error: null });

        const s = useTenantStore();
        await s.loadSessionAndTenant();

        expect(s.tenantId).toBe('t1');
        expect(s.currentTenantId).toBe('t1');
        expect(s.role).toBe('clinic_admin');
        expect(s.tenant).toEqual({ id: 't1', role: 'clinic_admin' });
        expect(s.hasActiveTenant).toBe(true);
    });

    it('tenant=null e hasActiveTenant=false sem activeTenantId', () => {
        const s = useTenantStore();
        expect(s.tenant).toBe(null);
        expect(s.hasActiveTenant).toBe(false);
    });
});
