-- ---------------------------------------------------------------------------
-- Prova que o isolamento entre inquilinos funciona de verdade.
--
--   docker compose exec -T postgres \
--     psql -U franquiabv -d franquiabv_dev -v ON_ERROR_STOP=1 -q \
--     < postgres/teste-rls.sql
--
-- Falha com código diferente de zero se qualquer asserção quebrar, então serve
-- direto no CI.
--
-- Este teste existe porque o vazamento entre inquilinos é o defeito mais caro
-- possível num SaaS: é silencioso, é o cliente que descobre, e não tem
-- desculpa. Ele precisa rodar ANTES de existir o inquilino nº 2.
-- ---------------------------------------------------------------------------

SET search_path = bvip, public;

-- ---------------------------------------------------------------------------
-- O papel da aplicação. Três exigências, e cada uma anula o RLS se faltar:
--
--   NOSUPERUSER   — superusuário IGNORA RLS sempre. ENABLE e FORCE não valem
--                   nada para ele. O usuário `franquiabv` deste ambiente é
--                   superusuário: se a aplicação conectar com ele, o
--                   isolamento simplesmente não existe.
--   NOBYPASSRLS   — o atributo BYPASSRLS faz o mesmo, de forma mais discreta.
--   não ser dono   — o dono da tabela escapa da política, a menos que ela tenha
--                   FORCE. O DDL usa FORCE justamente por isso, mas separar o
--                   dono do usuário da aplicação é a defesa em profundidade.
-- ---------------------------------------------------------------------------

-- O papel é criado pelo schema.sql. Aqui ele é apenas conferido: se o DDL
-- deixar de criá-lo, ou alguém der SUPERUSER a ele, o teste acusa na hora em
-- vez de passar dando a falsa impressão de que há isolamento.
DO $$
DECLARE
    ehSuper   boolean;
    ehBypass  boolean;
BEGIN
    SELECT rolsuper, rolbypassrls INTO ehSuper, ehBypass
      FROM pg_roles WHERE rolname = 'bvip_app';

    IF NOT FOUND THEN
        RAISE EXCEPTION 'O papel bvip_app não existe — rode postgres/schema.sql antes';
    END IF;

    IF ehSuper OR ehBypass THEN
        RAISE EXCEPTION
            'bvip_app tem SUPERUSER ou BYPASSRLS: o isolamento entre inquilinos NÃO está valendo';
    END IF;
END $$;

-- Cenário: dois inquilinos, um registro em cada.
INSERT INTO inquilinos (id, nome) VALUES (2, 'Rede Teste') ON CONFLICT DO NOTHING;

DELETE FROM saidas_tipos WHERE nome IN ('Tipo do inquilino 1', 'Tipo do inquilino 2');

INSERT INTO saidas_tipos (inquilino_id, nome, status) VALUES
    (1, 'Tipo do inquilino 1', true),
    (2, 'Tipo do inquilino 2', true);

-- ---------------------------------------------------------------------------

DO $$
DECLARE
    visiveis integer;
    bloqueou boolean;
BEGIN
    -- -----------------------------------------------------------------------
    -- 1. O superusuário ENXERGA TUDO, mesmo com RLS ligado e FORCE.
    --    Não é defeito do desenho: é como o PostgreSQL funciona. Está aqui
    --    para ficar registrado que a escolha do usuário de conexão é parte da
    --    segurança, não detalhe de infraestrutura.
    -- -----------------------------------------------------------------------
    PERFORM set_config('app.inquilino_id', '1', true);
    SELECT COUNT(*) INTO visiveis FROM saidas_tipos
     WHERE nome LIKE 'Tipo do inquilino%';

    IF visiveis <> 2 THEN
        RAISE EXCEPTION
            'Esperava que o superusuário enxergasse os 2 registros (RLS não se aplica a ele), mas viu %',
            visiveis;
    END IF;

    RAISE NOTICE 'ok  superusuário enxerga os 2 registros — RLS não se aplica a superusuário';

    -- -----------------------------------------------------------------------
    -- 2. Como a aplicação, o inquilino 1 só enxerga o dele.
    -- -----------------------------------------------------------------------
    SET LOCAL ROLE bvip_app;

    PERFORM set_config('app.inquilino_id', '1', true);
    SELECT COUNT(*) INTO visiveis FROM saidas_tipos
     WHERE nome LIKE 'Tipo do inquilino%';

    IF visiveis <> 1 THEN
        RAISE EXCEPTION 'VAZAMENTO: inquilino 1 enxergou % registros, esperado 1', visiveis;
    END IF;

    RAISE NOTICE 'ok  inquilino 1 enxerga apenas 1 registro';

    -- -----------------------------------------------------------------------
    -- 3. E o inquilino 2 só enxerga o dele.
    -- -----------------------------------------------------------------------
    PERFORM set_config('app.inquilino_id', '2', true);
    SELECT COUNT(*) INTO visiveis FROM saidas_tipos
     WHERE nome LIKE 'Tipo do inquilino%';

    IF visiveis <> 1 THEN
        RAISE EXCEPTION 'VAZAMENTO: inquilino 2 enxergou % registros, esperado 1', visiveis;
    END IF;

    RAISE NOTICE 'ok  inquilino 2 enxerga apenas 1 registro';

    -- -----------------------------------------------------------------------
    -- 4. WITH CHECK impede gravar no inquilino alheio, mesmo informando o id
    --    explicitamente. Sem isto, bastaria um id errado para contaminar a
    --    base de outro cliente.
    -- -----------------------------------------------------------------------
    bloqueou := false;

    BEGIN
        INSERT INTO saidas_tipos (inquilino_id, nome, status)
        VALUES (1, 'Invasao', true);
    EXCEPTION WHEN insufficient_privilege THEN
        bloqueou := true;
    END;

    IF NOT bloqueou THEN
        RAISE EXCEPTION 'FALHA: inquilino 2 conseguiu gravar registro do inquilino 1';
    END IF;

    RAISE NOTICE 'ok  WITH CHECK bloqueou gravação no inquilino alheio';

    -- -----------------------------------------------------------------------
    -- 5. UPDATE também não alcança linha de outro inquilino: ele simplesmente
    --    não a enxerga, então nenhuma linha é afetada.
    -- -----------------------------------------------------------------------
    UPDATE saidas_tipos SET nome = 'Alterado indevidamente'
     WHERE nome = 'Tipo do inquilino 1';

    GET DIAGNOSTICS visiveis = ROW_COUNT;

    IF visiveis <> 0 THEN
        RAISE EXCEPTION 'VAZAMENTO: inquilino 2 alterou % linha(s) do inquilino 1', visiveis;
    END IF;

    RAISE NOTICE 'ok  UPDATE não alcança linha de outro inquilino';

    -- -----------------------------------------------------------------------
    -- 6. Sem contexto de inquilino, nada é acessível. É o comportamento certo:
    --    falhar fechado. Uma conexão que esqueceu de declarar o inquilino não
    --    pode enxergar tudo.
    -- -----------------------------------------------------------------------
    PERFORM set_config('app.inquilino_id', '', true);

    bloqueou := false;

    BEGIN
        SELECT COUNT(*) INTO visiveis FROM saidas_tipos;
    EXCEPTION WHEN others THEN
        bloqueou := true;
    END;

    IF NOT bloqueou AND visiveis > 0 THEN
        RAISE EXCEPTION 'VAZAMENTO: sem contexto de inquilino, % registros ficaram visíveis', visiveis;
    END IF;

    RAISE NOTICE 'ok  sem contexto de inquilino o acesso falha fechado';

    RESET ROLE;
END $$;

-- Limpeza do cenário.
DELETE FROM saidas_tipos WHERE nome IN ('Tipo do inquilino 1', 'Tipo do inquilino 2', 'Invasao');
DELETE FROM inquilinos   WHERE id = 2;

SELECT 'RLS: todas as asserções passaram' AS resultado;
