#!/usr/bin/env node
// =============================================================================
// AgenciaPsi — RPC integration tests (T#8)
// =============================================================================
// Executa cenários SQL via `docker exec` no container do Postgres.
// Cada cenário roda em transação isolada (BEGIN ... ROLLBACK), zero side effect.
//
// Uso:  node database-novo/tests/run.cjs
//
// Estrutura de cada caso:
//   { name, sub: 'as user|as saas|anon', sql, expect: { ok|errorIncludes, jsonHas } }
// =============================================================================

const { execSync } = require('child_process');
const path = require('path');

const CONFIG = JSON.parse(require('fs').readFileSync(path.join(__dirname, '..', 'db.config.json'), 'utf8'));
const CONTAINER = CONFIG.container;
const DB = CONFIG.database;
const USER = CONFIG.user;

// ─────────────────────────────────────────────────────────────────────────
// Helpers
// -----------------------------------------------------------------------------
function runSql(sql) {
    const cmd = `docker exec -i -e PGCLIENTENCODING=UTF8 ${CONTAINER} psql -U ${USER} -d ${DB} -v ON_ERROR_STOP=1 -t -A`;
    try {
        const out = execSync(cmd, { input: sql, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
        return { ok: true, out: String(out).trim(), err: null };
    } catch (e) {
        const stderr = String(e?.stderr || e?.message || '').trim();
        return { ok: false, out: null, err: stderr };
    }
}

function asUser(uid) {
    return `SET LOCAL ROLE authenticated; SELECT set_config('request.jwt.claim.sub', '${uid}', true);`;
}

// IDs do seed que sabemos existir (db.config.json + seeds)
const SAAS_ADMIN_UID    = 'aaaaaaaa-0006-0006-0006-000000000006';
const TENANT_ADMIN_UID  = 'aaaaaaaa-0005-0005-0005-000000000005'; // owner de Clínica Bem Estar
const TENANT_BEM_ESTAR  = 'bbbbbbbb-0005-0005-0005-000000000005';
const PATIENT_UID       = 'aaaaaaaa-0001-0001-0001-000000000001';

// ─────────────────────────────────────────────────────────────────────────
// Cases
// -----------------------------------------------------------------------------
const cases = [
    // ────── set_tenant_feature_exception ──────
    {
        name: 'set_tenant_feature_exception: anônimo é rejeitado',
        sql: `BEGIN;
              SET LOCAL ROLE authenticated;
              SELECT public.set_tenant_feature_exception('${TENANT_BEM_ESTAR}'::uuid, 'patients', true, NULL);
              ROLLBACK;`,
        expect: { errorIncludes: 'Não autenticado' }
    },
    {
        name: 'set_tenant_feature_exception: tenant_admin tenta override+ fora do plano → rejeitado',
        sql: `BEGIN;
              ${asUser(TENANT_ADMIN_UID)}
              SELECT public.set_tenant_feature_exception('${TENANT_BEM_ESTAR}'::uuid, 'documents.signatures', true, 'tentativa');
              ROLLBACK;`,
        expect: { errorIncludes: 'saas_admin' }
    },
    {
        name: 'set_tenant_feature_exception: saas_admin sem reason em exceção → rejeitado',
        sql: `BEGIN;
              ${asUser(SAAS_ADMIN_UID)}
              SELECT public.set_tenant_feature_exception('${TENANT_BEM_ESTAR}'::uuid, 'documents.signatures', true, NULL);
              ROLLBACK;`,
        expect: { errorIncludes: 'reason' }
    },
    {
        name: 'set_tenant_feature_exception: saas_admin com reason curto (<4 chars) — RPC aceita, frontend valida',
        sql: `BEGIN;
              ${asUser(SAAS_ADMIN_UID)}
              SELECT (public.set_tenant_feature_exception('${TENANT_BEM_ESTAR}'::uuid, 'documents.signatures', true, 'PIX'))->>'is_exception';
              ROLLBACK;`,
        expect: { ok: true, jsonHas: 'true' } // is_exception=true
    },
    {
        name: 'set_tenant_feature_exception: tenant_admin desliga feature DO plano → permitido',
        sql: `BEGIN;
              ${asUser(TENANT_ADMIN_UID)}
              SELECT (public.set_tenant_feature_exception('${TENANT_BEM_ESTAR}'::uuid, 'patients', false, 'pref'))->>'plan_allows';
              ROLLBACK;`,
        expect: { ok: true, jsonHas: 'true' }
    },
    {
        name: 'set_tenant_feature_exception: feature_key inválida (uppercase) → rejeitado',
        sql: `BEGIN;
              ${asUser(SAAS_ADMIN_UID)}
              SELECT public.set_tenant_feature_exception('${TENANT_BEM_ESTAR}'::uuid, 'PATIENTS', false, NULL);
              ROLLBACK;`,
        expect: { errorIncludes: 'formato inválido' }
    },
    {
        name: 'set_tenant_feature_exception: feature_key desconhecida → rejeitado',
        sql: `BEGIN;
              ${asUser(SAAS_ADMIN_UID)}
              SELECT public.set_tenant_feature_exception('${TENANT_BEM_ESTAR}'::uuid, 'unknown_feature', false, NULL);
              ROLLBACK;`,
        expect: { errorIncludes: 'desconhecida' }
    },
    {
        name: 'set_tenant_feature_exception: tenant inexistente → rejeitado',
        sql: `BEGIN;
              ${asUser(SAAS_ADMIN_UID)}
              SELECT public.set_tenant_feature_exception('99999999-9999-9999-9999-999999999999'::uuid, 'patients', false, NULL);
              ROLLBACK;`,
        expect: { errorIncludes: 'tenant não encontrado' }
    },
    {
        name: 'set_tenant_feature_exception: trigger guard ainda bloqueia INSERT direto fora do plano',
        sql: `BEGIN;
              INSERT INTO tenant_features (tenant_id, feature_key, enabled)
              VALUES ('${TENANT_BEM_ESTAR}', 'documents.signatures', true);
              ROLLBACK;`,
        expect: { errorIncludes: 'não permitida pelo plano' }
    },

    // ────── delete_plan_safe ──────
    {
        name: 'delete_plan_safe: anônimo é rejeitado',
        sql: `BEGIN;
              SET LOCAL ROLE authenticated;
              SELECT public.delete_plan_safe((SELECT id FROM plans WHERE key='clinic_free'));
              ROLLBACK;`,
        expect: { errorIncludes: 'Não autenticado' }
    },
    {
        name: 'delete_plan_safe: tenant_admin não-saas é rejeitado',
        sql: `BEGIN;
              ${asUser(TENANT_ADMIN_UID)}
              SELECT public.delete_plan_safe((SELECT id FROM plans WHERE key='clinic_free'));
              ROLLBACK;`,
        expect: { errorIncludes: 'saas_admin' }
    },
    {
        name: 'delete_plan_safe: bloqueia delete de plano com subscriptions ativas',
        sql: `BEGIN;
              ${asUser(SAAS_ADMIN_UID)}
              SELECT public.delete_plan_safe((SELECT id FROM plans WHERE key='clinic_free'));
              ROLLBACK;`,
        expect: { errorIncludes: 'assinatura' }
    },
    {
        name: 'delete_plan_safe: plan_id null → rejeitado',
        sql: `BEGIN;
              ${asUser(SAAS_ADMIN_UID)}
              SELECT public.delete_plan_safe(NULL);
              ROLLBACK;`,
        expect: { errorIncludes: 'plan_id obrigatório' }
    },
    {
        name: 'delete_plan_safe: plano inexistente → rejeitado',
        sql: `BEGIN;
              ${asUser(SAAS_ADMIN_UID)}
              SELECT public.delete_plan_safe('99999999-9999-9999-9999-999999999999'::uuid);
              ROLLBACK;`,
        expect: { errorIncludes: 'plano não encontrado' }
    },

    // ────── create_patient_intake_request_v2 ──────
    {
        name: 'create_patient_intake_request_v2: A#20 — anon NÃO chama mais o RPC direto',
        sql: `BEGIN;
              SET LOCAL ROLE anon;
              SELECT public.create_patient_intake_request_v2('token-inexistente'::text, '{}'::jsonb, NULL::text);
              ROLLBACK;`,
        expect: { errorIncludes: 'permission denied' }
    },
    {
        name: 'create_patient_intake_request_v2: token inválido (via authenticated) → Token inválido',
        sql: `BEGIN;
              ${asUser(TENANT_ADMIN_UID)}
              SELECT public.create_patient_intake_request_v2('token-inexistente'::text, '{}'::jsonb, NULL::text);
              ROLLBACK;`,
        expect: { errorIncludes: 'Token inválido' }
    },
    {
        name: 'create_patient_intake_request_v2: payload sem nome_completo → rejeitado',
        sql: `BEGIN;
              -- desativa invites pré-existentes desse owner (constraint one_active_per_owner)
              UPDATE patient_invites SET active=false WHERE owner_id='${TENANT_ADMIN_UID}' AND active=true;
              WITH inv AS (
                INSERT INTO patient_invites (token, owner_id, tenant_id, active)
                VALUES ('test-token-' || md5(random()::text), '${TENANT_ADMIN_UID}', '${TENANT_BEM_ESTAR}', true)
                RETURNING token
              )
              SELECT public.create_patient_intake_request_v2(
                (SELECT token FROM inv),
                jsonb_build_object('email_principal','test@x.com','telefone','11999999999','consent',true),
                NULL::text
              );
              ROLLBACK;`,
        expect: { errorIncludes: 'Nome' }
    },

    // ────── features.is_active (V#40 sanity) ──────
    {
        name: 'features.is_active existe e default true',
        sql: `SELECT column_default FROM information_schema.columns WHERE table_name='features' AND column_name='is_active';`,
        expect: { ok: true, jsonHas: 'true' }
    },

    // ────── A#20 rev2: defesa em camadas ──────
    {
        name: 'check_rate_limit: IP novo → allowed=true',
        sql: `BEGIN;
              SET LOCAL ROLE service_role;
              SELECT (public.check_rate_limit('test-hash-novo-' || gen_random_uuid()::text, 'patient_intake'))->>'allowed';
              ROLLBACK;`,
        expect: { ok: true, jsonHas: 'true' }
    },
    {
        name: 'record_submission_attempt: 3 falhas seguidas → marca requires_captcha',
        sql: `BEGIN;
              SET LOCAL ROLE service_role;
              SELECT public.record_submission_attempt('patient_intake', 'h-fail-3', false, 'rpc', 'X', 'Y', NULL, NULL);
              SELECT public.record_submission_attempt('patient_intake', 'h-fail-3', false, 'rpc', 'X', 'Y', NULL, NULL);
              SELECT public.record_submission_attempt('patient_intake', 'h-fail-3', false, 'rpc', 'X', 'Y', NULL, NULL);
              SELECT (public.check_rate_limit('h-fail-3', 'patient_intake'))->>'requires_captcha';
              ROLLBACK;`,
        expect: { ok: true, jsonHas: 'true' }
    },
    {
        name: 'record_submission_attempt: > max_attempts → bloqueia (allowed=false)',
        sql: `BEGIN;
              SET LOCAL ROLE service_role;
              SELECT public.record_submission_attempt('patient_intake', 'h-block', false, 'rpc', 'X', 'Y', NULL, NULL);
              SELECT public.record_submission_attempt('patient_intake', 'h-block', false, 'rpc', 'X', 'Y', NULL, NULL);
              SELECT public.record_submission_attempt('patient_intake', 'h-block', false, 'rpc', 'X', 'Y', NULL, NULL);
              SELECT public.record_submission_attempt('patient_intake', 'h-block', false, 'rpc', 'X', 'Y', NULL, NULL);
              SELECT public.record_submission_attempt('patient_intake', 'h-block', false, 'rpc', 'X', 'Y', NULL, NULL);
              SELECT (public.check_rate_limit('h-block', 'patient_intake'))->>'allowed';
              ROLLBACK;`,
        expect: { ok: true, jsonHas: 'false' }
    },
    {
        name: 'generate_math_challenge: cria id + question',
        sql: `BEGIN;
              SET LOCAL ROLE service_role;
              SELECT (public.generate_math_challenge())->>'question';
              ROLLBACK;`,
        expect: { ok: true, jsonHas: 'Quanto' }
    },
    {
        name: 'verify_math_challenge: id desconhecido → false',
        sql: `BEGIN;
              SET LOCAL ROLE service_role;
              SELECT public.verify_math_challenge('00000000-0000-0000-0000-000000000000'::uuid, 1)::text;
              ROLLBACK;`,
        expect: { ok: true, jsonHas: 'f' }
    },
    {
        name: 'check_rate_limit: anon não pode chamar (só service_role)',
        sql: `BEGIN;
              SET LOCAL ROLE anon;
              SELECT public.check_rate_limit('h', 'patient_intake');
              ROLLBACK;`,
        expect: { errorIncludes: 'permission denied' }
    },
    {
        name: 'saas_security_config singleton existe com defaults',
        sql: `SELECT honeypot_enabled::text || ',' || rate_limit_enabled::text FROM saas_security_config WHERE id=true;`,
        expect: { ok: true, jsonHas: 'true,true' }
    },

    // ────── saas_twilio_config + RPCs ──────
    {
        name: 'get_twilio_config: anon NÃO pode chamar',
        sql: `BEGIN;
              SET LOCAL ROLE anon;
              SELECT public.get_twilio_config();
              ROLLBACK;`,
        expect: { errorIncludes: 'permission denied' }
    },
    {
        name: 'get_twilio_config: authenticated não-saas → "Sem permissão"',
        sql: `BEGIN;
              ${asUser(TENANT_ADMIN_UID)}
              SELECT public.get_twilio_config();
              ROLLBACK;`,
        expect: { errorIncludes: 'Sem permissão' }
    },
    {
        name: 'get_twilio_config: saas_admin retorna defaults',
        sql: `BEGIN;
              ${asUser(SAAS_ADMIN_UID)}
              SELECT (public.get_twilio_config())->>'usd_brl_rate';
              ROLLBACK;`,
        expect: { ok: true, jsonHas: '5.5' }
    },
    {
        name: 'update_twilio_config: tenant_admin é rejeitado',
        sql: `BEGIN;
              ${asUser(TENANT_ADMIN_UID)}
              SELECT public.update_twilio_config(p_usd_brl_rate := 6);
              ROLLBACK;`,
        expect: { errorIncludes: 'saas_admin' }
    },
    {
        name: 'update_twilio_config: SID inválido (sem AC) → rejeitado',
        sql: `BEGIN;
              ${asUser(SAAS_ADMIN_UID)}
              SELECT public.update_twilio_config(p_account_sid := 'XX123');
              ROLLBACK;`,
        expect: { errorIncludes: 'account_sid inválido' }
    },
    {
        name: 'update_twilio_config: webhook sem http(s) → rejeitado',
        sql: `BEGIN;
              ${asUser(SAAS_ADMIN_UID)}
              SELECT public.update_twilio_config(p_whatsapp_webhook_url := 'ftp://x.com');
              ROLLBACK;`,
        expect: { errorIncludes: 'webhook' }
    },
    {
        name: 'update_twilio_config: rate fora da faixa → rejeitado',
        sql: `BEGIN;
              ${asUser(SAAS_ADMIN_UID)}
              SELECT public.update_twilio_config(p_usd_brl_rate := 200);
              ROLLBACK;`,
        expect: { errorIncludes: 'usd_brl_rate' }
    },
    {
        name: 'update_twilio_config: saas_admin com payload válido → ok',
        sql: `BEGIN;
              ${asUser(SAAS_ADMIN_UID)}
              SELECT (public.update_twilio_config(
                p_account_sid := 'ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
                p_usd_brl_rate := 5.85,
                p_margin_multiplier := 1.5
              ))->>'usd_brl_rate';
              ROLLBACK;`,
        expect: { ok: true, jsonHas: '5.85' }
    }
];

// ─────────────────────────────────────────────────────────────────────────
// Runner
// -----------------------------------------------------------------------------
function color(s, c) {
    const map = { red: 31, green: 32, yellow: 33, gray: 90 };
    return `\x1b[${map[c] || 0}m${s}\x1b[0m`;
}

let pass = 0;
let fail = 0;
const failures = [];

console.log(color('▶ AgenciaPsi RPC integration tests', 'gray'));
console.log(color('───────────────────────────────────', 'gray'));

for (const c of cases) {
    process.stdout.write(`  ${c.name} ... `);
    const r = runSql(c.sql);

    let ok = true;
    let why = '';

    if (c.expect.errorIncludes) {
        if (r.ok) {
            ok = false;
            why = `esperava erro com "${c.expect.errorIncludes}" mas SQL passou. saída=${r.out}`;
        } else if (!r.err.includes(c.expect.errorIncludes)) {
            ok = false;
            why = `erro não contém "${c.expect.errorIncludes}". stderr=${r.err.slice(0, 200)}`;
        }
    } else {
        if (!r.ok) {
            ok = false;
            why = `esperava sucesso mas falhou. stderr=${r.err.slice(0, 300)}`;
        } else if (c.expect.jsonHas && !r.out.includes(c.expect.jsonHas)) {
            ok = false;
            why = `saída não contém "${c.expect.jsonHas}". out=${r.out}`;
        }
    }

    if (ok) {
        pass++;
        console.log(color('✓', 'green'));
    } else {
        fail++;
        failures.push({ name: c.name, why });
        console.log(color('✗', 'red'));
    }
}

console.log(color('───────────────────────────────────', 'gray'));
console.log(`${color(pass + ' passed', 'green')}, ${fail > 0 ? color(fail + ' failed', 'red') : color('0 failed', 'gray')}`);

if (failures.length) {
    console.log('\n' + color('FAILURES:', 'red'));
    for (const f of failures) {
        console.log(`  ${color('×', 'red')} ${f.name}`);
        console.log(`    ${color(f.why, 'yellow')}`);
    }
    process.exit(1);
}

process.exit(0);
