-- ============================================================
-- Tests pgTAP: Etapa 1 — Esquema, RLS, Constraints, k-anonimato
--
-- Ejecutar con: supabase test db
-- Requiere: Docker + supabase start + extensión pgtap
--
-- Cubre las 6 validaciones mandatorias del ETAPA_1_PROMPT.md:
--   1. Migración desde cero sin errores (verificado al correr supabase db reset)
--   2. Esquema coincide con DATA_MODEL.md
--   3. RLS: org_admin no ve filas crudas de responses ni answers
--   4. Constraint: respondent_id en encuesta anónima → error
--   5. k-anonimato: agg_segment n=3 → suppressed; n=5 → valor real
--   6. Inmutabilidad: UPDATE/DELETE en consent_records → error
-- ============================================================

BEGIN;

SELECT plan(25);

-- ============================================================
-- SECCIÓN 1: Estructura del esquema (7 tablas + columnas clave)
-- ============================================================

SELECT has_table('public', 'organizations',    '1.1 tabla organizations existe');
SELECT has_table('public', 'consent_versions', '1.2 tabla consent_versions existe');
SELECT has_table('public', 'surveys',          '1.3 tabla surveys existe');
SELECT has_table('public', 'questions',        '1.4 tabla questions existe');
SELECT has_table('public', 'consent_records',  '1.5 tabla consent_records existe');
SELECT has_table('public', 'responses',        '1.6 tabla responses existe');
SELECT has_table('public', 'answers',          '1.7 tabla answers existe');

-- respondent_id debe existir y ser nullable (reservado pero NULL en v1)
SELECT has_column('public', 'responses', 'respondent_id',
    '1.8 responses.respondent_id existe (reservado)');
SELECT col_is_null('public', 'responses', 'respondent_id',
    '1.9 responses.respondent_id es nullable');

-- función agg_segment debe existir
SELECT has_function('public', 'agg_segment', ARRAY['uuid', 'text[]'],
    '1.10 función agg_segment(uuid, text[]) existe');

-- ============================================================
-- SECCIÓN 2: Constraint — respondent_id en encuesta anónima
-- (Regla no negociable #2 de CLAUDE.md)
-- ============================================================
DO $$
DECLARE
    v_org_id    uuid := '10000000-ffff-0000-0000-000000000001';
    v_cv_id     uuid := '20000000-ffff-0000-0000-000000000001';
    v_survey_id uuid := '30000000-ffff-0000-0000-000000000001';
    v_cr_id     uuid := '50000000-ffff-0000-0000-000000000001';
BEGIN
    INSERT INTO public.organizations (id, name) VALUES (v_org_id, '_test_constraint_org');
    INSERT INTO public.consent_versions (id, version, body, scopes)
        VALUES (v_cv_id, '_test-cv-constraint', 'body', '["operativo"]'::jsonb);
    INSERT INTO public.surveys (id, org_id, title, is_anonymous, data_sensitivity, security_class)
        VALUES (v_survey_id, v_org_id, '_test_anon_survey', true, 'sensible', 'confidencial');
    INSERT INTO public.consent_records (id, consent_version_id, granted_scopes)
        VALUES (v_cr_id, v_cv_id, '{"operativo": true}'::jsonb);
END;
$$;

SELECT throws_ok(
    format(
        'INSERT INTO public.responses (survey_id, company_id, respondent_id, consent_record_id)
         VALUES (%L, %L, gen_random_uuid(), %L)',
        '30000000-ffff-0000-0000-000000000001',
        '10000000-ffff-0000-0000-000000000001',
        '50000000-ffff-0000-0000-000000000001'
    ),
    '23514',   -- check_violation SQLSTATE
    NULL,
    '2.1 respondent_id en encuesta anónima → check_violation'
);

-- Una response SIN respondent_id en encuesta anónima debe poder insertarse
SELECT lives_ok(
    format(
        'INSERT INTO public.responses (survey_id, company_id, consent_record_id, qi_age_bucket)
         VALUES (%L, %L, %L, %L)',
        '30000000-ffff-0000-0000-000000000001',
        '10000000-ffff-0000-0000-000000000001',
        '50000000-ffff-0000-0000-000000000001',
        '25-34'
    ),
    '2.2 response sin respondent_id en encuesta anónima → permitido'
);

-- ============================================================
-- SECCIÓN 3: Inmutabilidad de consent_records
-- (ADR-003: evidencia permanente del consentimiento)
-- ============================================================
DO $$
DECLARE
    v_cv_id uuid := '20000000-eeee-0000-0000-000000000001';
    v_cr_id uuid := '50000000-eeee-0000-0000-000000000001';
BEGIN
    INSERT INTO public.consent_versions (id, version, body, scopes)
        VALUES (v_cv_id, '_test-cv-immutable', 'body', '["operativo"]'::jsonb);
    INSERT INTO public.consent_records (id, consent_version_id, granted_scopes)
        VALUES (v_cr_id, v_cv_id, '{"operativo": true}'::jsonb);
END;
$$;

SELECT throws_ok(
    format(
        'UPDATE public.consent_records SET granted_at = now() WHERE id = %L',
        '50000000-eeee-0000-0000-000000000001'
    ),
    '42501',   -- insufficient_privilege SQLSTATE
    NULL,
    '3.1 UPDATE en consent_records → error (inmutabilidad)'
);

SELECT throws_ok(
    format(
        'DELETE FROM public.consent_records WHERE id = %L',
        '50000000-eeee-0000-0000-000000000001'
    ),
    '42501',
    NULL,
    '3.2 DELETE en consent_records → error (inmutabilidad)'
);

-- ============================================================
-- SECCIÓN 4: k-anonimato — agg_segment
-- k_threshold=5: n=3 debe estar suprimido; n=5 debe mostrar valor
-- ============================================================
DO $$
DECLARE
    v_org_id    uuid := '10000000-dddd-0000-0000-000000000001';
    v_cv_id     uuid := '20000000-dddd-0000-0000-000000000001';
    v_survey_id uuid := '30000000-dddd-0000-0000-000000000001';
    v_cr_id     uuid;
    i           int;
BEGIN
    INSERT INTO public.organizations (id, name) VALUES (v_org_id, '_test_kanonimato_org');
    INSERT INTO public.consent_versions (id, version, body, scopes)
        VALUES (v_cv_id, '_test-cv-kanon', 'body', '["operativo"]'::jsonb);
    INSERT INTO public.surveys (
        id, org_id, title, is_anonymous, data_sensitivity, security_class, k_threshold
    )
    VALUES (v_survey_id, v_org_id, '_test_kanonimato_survey', true, 'comun', 'confidencial', 5);

    -- Insertar 3 responses en el mismo segmento (25-34 / Salud)
    FOR i IN 1..3 LOOP
        v_cr_id := gen_random_uuid();
        INSERT INTO public.consent_records (id, consent_version_id, granted_scopes)
            VALUES (v_cr_id, v_cv_id, '{"operativo": true}'::jsonb);
        INSERT INTO public.responses (survey_id, company_id, consent_record_id, qi_age_bucket, qi_sector)
            VALUES (v_survey_id, v_org_id, v_cr_id, '25-34', 'Salud');
    END LOOP;
END;
$$;

-- n=3 → suppressed debe ser TRUE, n debe ser NULL
SELECT ok(
    (SELECT suppressed
     FROM public.agg_segment('30000000-dddd-0000-0000-000000000001', ARRAY['qi_age_bucket'])
     WHERE (segment->>'qi_age_bucket') = '25-34'),
    '4.1 agg_segment n=3 → suppressed=true'
);

SELECT is(
    (SELECT n
     FROM public.agg_segment('30000000-dddd-0000-0000-000000000001', ARRAY['qi_age_bucket'])
     WHERE (segment->>'qi_age_bucket') = '25-34'),
    NULL::bigint,
    '4.2 agg_segment n=3 → n=NULL (celda suprimida)'
);

-- Agregar 2 responses más → total 5
DO $$
DECLARE
    v_cv_id     uuid := '20000000-dddd-0000-0000-000000000001';
    v_survey_id uuid := '30000000-dddd-0000-0000-000000000001';
    v_org_id    uuid := '10000000-dddd-0000-0000-000000000001';
    v_cr_id     uuid;
    i           int;
BEGIN
    FOR i IN 1..2 LOOP
        v_cr_id := gen_random_uuid();
        INSERT INTO public.consent_records (id, consent_version_id, granted_scopes)
            VALUES (v_cr_id, v_cv_id, '{"operativo": true}'::jsonb);
        INSERT INTO public.responses (survey_id, company_id, consent_record_id, qi_age_bucket, qi_sector)
            VALUES (v_survey_id, v_org_id, v_cr_id, '25-34', 'Salud');
    END LOOP;
END;
$$;

-- n=5 → suppressed debe ser FALSE, n debe ser 5
SELECT ok(
    NOT (SELECT suppressed
         FROM public.agg_segment('30000000-dddd-0000-0000-000000000001', ARRAY['qi_age_bucket'])
         WHERE (segment->>'qi_age_bucket') = '25-34'),
    '4.3 agg_segment n=5 → suppressed=false'
);

SELECT is(
    (SELECT n
     FROM public.agg_segment('30000000-dddd-0000-0000-000000000001', ARRAY['qi_age_bucket'])
     WHERE (segment->>'qi_age_bucket') = '25-34'),
    5::bigint,
    '4.4 agg_segment n=5 → n=5 (valor real visible)'
);

-- ============================================================
-- SECCIÓN 5: RLS — org_admin no puede ver filas crudas
-- Se simula el rol 'authenticated' con JWT claim app_role='org_admin'
-- ============================================================
DO $$
DECLARE
    v_org_a uuid := '10000000-aaaa-0000-0000-000000000001';
    v_cv_id uuid := '20000000-aaaa-0000-0000-000000000001';
    v_srv_id uuid := '30000000-aaaa-0000-0000-000000000001';
    v_cr_id  uuid := gen_random_uuid();
    v_resp_count bigint;
    v_ans_count  bigint;
BEGIN
    -- Setup: crear org, encuesta y una response
    INSERT INTO public.organizations (id, name) VALUES (v_org_a, '_test_rls_org_a');
    INSERT INTO public.consent_versions (id, version, body, scopes)
        VALUES (v_cv_id, '_test-cv-rls', 'body', '["operativo"]'::jsonb);
    INSERT INTO public.surveys (id, org_id, title, is_anonymous, data_sensitivity, security_class, status)
        VALUES (v_srv_id, v_org_a, '_test_rls_survey', true, 'comun', 'confidencial', 'live');
    INSERT INTO public.consent_records (id, consent_version_id, granted_scopes)
        VALUES (v_cr_id, v_cv_id, '{"operativo": true}'::jsonb);
    INSERT INTO public.responses (survey_id, company_id, consent_record_id, qi_age_bucket)
        VALUES (v_srv_id, v_org_a, v_cr_id, '35-44');

    -- Simular JWT de org_admin de v_org_a
    PERFORM set_config('request.jwt.claims',
        json_build_object('app_role', 'org_admin', 'org_id', v_org_a::text)::text,
        true
    );

    -- Cambiar al rol authenticated (con RLS activo)
    SET LOCAL ROLE authenticated;

    SELECT count(*) INTO v_resp_count FROM public.responses;
    SELECT count(*) INTO v_ans_count  FROM public.answers;

    RESET ROLE;

    -- Limpiar config
    PERFORM set_config('request.jwt.claims', '', true);

    IF v_resp_count != 0 THEN
        RAISE EXCEPTION
            'RLS FAIL: org_admin ve % filas crudas en responses (esperado: 0)',
            v_resp_count;
    END IF;

    IF v_ans_count != 0 THEN
        RAISE EXCEPTION
            'RLS FAIL: org_admin ve % filas crudas en answers (esperado: 0)',
            v_ans_count;
    END IF;
END;
$$;

SELECT pass('5.1 RLS: org_admin ve 0 filas crudas en responses');
SELECT pass('5.2 RLS: org_admin ve 0 filas crudas en answers');

-- ============================================================
-- SECCIÓN 6: Verificación del seed
-- ============================================================
SELECT is(
    (SELECT count(*)::int FROM public.questions
     WHERE survey_id = '30000000-0000-0000-0000-000000000001'),
    9,
    '6.1 Seed: encuesta tiene 9 preguntas'
);

SELECT is(
    (SELECT count(*)::int FROM public.questions
     WHERE survey_id = '30000000-0000-0000-0000-000000000001'
       AND jsonb_array_length(COALESCE(conditional_rules, '[]'::jsonb)) > 0),
    3,
    '6.2 Seed: 3 preguntas tienen conditional_rules (B1, C1, D1 declaradas)'
);

-- B1 tiene exactamente 3 reglas condicionales (→ B2, B4, B5)
SELECT is(
    (SELECT jsonb_array_length(conditional_rules)
     FROM public.questions
     WHERE survey_id = '30000000-0000-0000-0000-000000000001' AND code = 'B1'),
    3,
    '6.3 Seed: B1 tiene 3 reglas condicionales (despliega B2, B4, B5)'
);

-- H1 tiene max_select=3
SELECT is(
    (SELECT max_select FROM public.questions
     WHERE survey_id = '30000000-0000-0000-0000-000000000001' AND code = 'H1'),
    3,
    '6.4 Seed: H1 max_select=3'
);

-- B5 es la única pregunta marcada is_sensitive=true
SELECT is(
    (SELECT count(*)::int FROM public.questions
     WHERE survey_id = '30000000-0000-0000-0000-000000000001' AND is_sensitive = true),
    1,
    '6.5 Seed: exactamente 1 pregunta is_sensitive=true (B5)'
);

SELECT * FROM finish();
ROLLBACK;
