// Tests Node puros (sin navegador, sin DB) del validador server-side de submit.
// Ejecutar: node --test lib/submit-validation.test.mts
import { test, describe } from 'node:test'
import assert from 'node:assert/strict'
import { buildFormSchema } from './form-engine.ts'
import { validateSubmission } from './submit-validation.ts'
import type { ConditionalRule, Question } from './form-engine.types.ts'
import type { SubmitAnswerEntry } from './submit-validation.ts'

function q(partial: Partial<Question> & Pick<Question, 'code' | 'type' | 'prompt' | 'position'>): Question {
  return {
    id: partial.code,
    is_sensitive: false,
    required: false,
    hint: null,
    options: null,
    max_select: null,
    conditional_rules: null,
    instance_generator: false,
    instance_of: null,
    ...partial,
  }
}

const r_show_si_3_1: ConditionalRule[] = [
  { if_question: '3.1', op: 'eq', value: 'Sí', action: 'show' },
]

const VINCULOS = ['Hijo menor con discapacidad', 'Familiar mayor con discapacidad', 'Otro / Especificar']

function buildFixtureSchema() {
  const rows: Question[] = [
    q({ code: 'S2', type: 'section', prompt: 'Datos generales', position: 20 }),
    q({
      code: '2.5', type: 'single_choice', prompt: 'Género', position: 25, required: true,
      options: [{ value: 'Femenino', label: 'Femenino' }, { value: 'Masculino', label: 'Masculino' }],
    }),

    q({ code: 'S3', type: 'section', prompt: 'Prevalencia', position: 30 }),
    q({
      code: '3.1', type: 'single_choice', prompt: '¿Tenés discapacidad?', position: 31,
      options: [{ value: 'Sí', label: 'Sí' }, { value: 'No', label: 'No' }],
    }),
    q({
      code: '3.2', type: 'multiple_choice', prompt: 'Tipo', position: 32, conditional_rules: r_show_si_3_1,
      options: [{ value: 'Visual', label: 'Visual' }, { value: 'Auditiva', label: 'Auditiva' }],
    }),

    q({ code: 'S4', type: 'section', prompt: 'Vinculación familiar', position: 40 }),
    q({
      code: '4.1', type: 'multiple_choice', prompt: 'Vínculos', position: 41, instance_generator: true,
      options: VINCULOS.map((v) => ({ value: v, label: v })),
    }),
    q({
      code: '4.2', type: 'multiple_choice', prompt: 'Tipo familiar', position: 42, instance_of: '4.1',
      max_select: 2,
      options: [{ value: 'Visual', label: 'Visual' }, { value: 'Auditiva', label: 'Auditiva' }],
    }),
  ]
  return buildFormSchema(rows)
}

// ── Happy path: 2 familiares ──────────────────────────────────────────────
test('happy path: submit válido con 2 familiares — payload completo con instance_id', () => {
  const schema = buildFixtureSchema()
  const entries: SubmitAnswerEntry[] = [
    { code: '2.5', instanceId: 'default', value: 'Femenino' },
    { code: '3.1', instanceId: 'default', value: 'No' }, // oculta 3.2 — no se manda
    { code: '4.1', instanceId: 'default', value: [VINCULOS[0], VINCULOS[1]] },
    { code: '4.2', instanceId: 'familiar_1', value: ['Visual'] },
    { code: '4.2', instanceId: 'familiar_2', value: ['Auditiva'] },
  ]

  const result = validateSubmission(schema, entries, true)
  assert.equal(result.ok, true)
  if (!result.ok) return

  // ── Dato concreto pedido en el reporte: filas reales con instance_id ──
  console.log('\nPayload validado → filas listas para la RPC (questionId/instanceId/value):')
  console.log(JSON.stringify(result.rows, null, 2))

  assert.equal(result.rows.length, 5)
  const f1 = result.rows.find((r) => r.questionId === '4.2' && r.instanceId === 'familiar_1')
  const f2 = result.rows.find((r) => r.questionId === '4.2' && r.instanceId === 'familiar_2')
  assert.deepEqual(f1?.value, ['Visual'])
  assert.deepEqual(f2?.value, ['Auditiva'])
})

describe('rechazos — las 7 validaciones', () => {
  test('rechaza respuesta a una pregunta que el flujo nunca debió mostrar (3.2 con 3.1=No)', () => {
    const schema = buildFixtureSchema()
    const entries: SubmitAnswerEntry[] = [
      { code: '2.5', instanceId: 'default', value: 'Femenino' },
      { code: '3.1', instanceId: 'default', value: 'No' },
      { code: '3.2', instanceId: 'default', value: ['Visual'] }, // 3.1='No' la oculta
    ]
    const result = validateSubmission(schema, entries, true)
    assert.equal(result.ok, false)
    if (result.ok) return
    assert.ok(result.errors.some((e) => e.startsWith('question_should_be_hidden:3.2')))
  })

  test('rechaza valor fuera de las opciones válidas', () => {
    const schema = buildFixtureSchema()
    const entries: SubmitAnswerEntry[] = [
      { code: '2.5', instanceId: 'default', value: 'Femenino' },
      { code: '3.1', instanceId: 'default', value: 'Tal vez' }, // no es 'Sí' ni 'No'
    ]
    const result = validateSubmission(schema, entries, true)
    assert.equal(result.ok, false)
    if (result.ok) return
    assert.ok(result.errors.some((e) => e.includes('value_not_in_options') && e.includes('3.1')))
  })

  test('rechaza max_select excedido (4.2 tiene max_select=2)', () => {
    const schema = buildFixtureSchema()
    const entries: SubmitAnswerEntry[] = [
      { code: '2.5', instanceId: 'default', value: 'Femenino' },
      { code: '4.1', instanceId: 'default', value: [VINCULOS[0]] },
      { code: '4.2', instanceId: 'familiar_1', value: ['Visual', 'Auditiva', 'Visual'] }, // 3 > max_select(2)
    ]
    const result = validateSubmission(schema, entries, true)
    assert.equal(result.ok, false)
    if (result.ok) return
    assert.ok(result.errors.some((e) => e.includes('exceeds_max_select')))
  })

  test('rechaza instancia inventada (familiar que 4.1 no generó)', () => {
    const schema = buildFixtureSchema()
    const entries: SubmitAnswerEntry[] = [
      { code: '2.5', instanceId: 'default', value: 'Femenino' },
      { code: '4.1', instanceId: 'default', value: [VINCULOS[0]] }, // genera solo familiar_1
      { code: '4.2', instanceId: 'familiar_2', value: ['Visual'] }, // familiar_2 NO existe
    ]
    const result = validateSubmission(schema, entries, true)
    assert.equal(result.ok, false)
    if (result.ok) return
    assert.ok(result.errors.some((e) => e.startsWith('invalid_instance:4.2:familiar_2')))
  })

  test('rechaza instance_id en una pregunta que no es de instancia', () => {
    const schema = buildFixtureSchema()
    const entries: SubmitAnswerEntry[] = [
      { code: '2.5', instanceId: 'familiar_1', value: 'Femenino' }, // 2.5 no es instance_of nada
    ]
    const result = validateSubmission(schema, entries, true)
    assert.equal(result.ok, false)
    if (result.ok) return
    assert.ok(result.errors.some((e) => e.startsWith('unexpected_instance_id:2.5')))
  })

  test('rechaza sin consentimiento operativo', () => {
    const schema = buildFixtureSchema()
    const entries: SubmitAnswerEntry[] = [{ code: '2.5', instanceId: 'default', value: 'Femenino' }]
    const result = validateSubmission(schema, entries, false)
    assert.equal(result.ok, false)
    if (result.ok) return
    assert.ok(result.errors.includes('consent_operativo_required'))
  })

  test('rechaza pregunta requerida visible sin responder', () => {
    const schema = buildFixtureSchema()
    // 2.5 es required=true y no se manda
    const entries: SubmitAnswerEntry[] = [{ code: '3.1', instanceId: 'default', value: 'No' }]
    const result = validateSubmission(schema, entries, true)
    assert.equal(result.ok, false)
    if (result.ok) return
    assert.ok(result.errors.some((e) => e.startsWith('required_missing:2.5')))
  })

  test('rechaza pregunta que no existe en el esquema (code inventado)', () => {
    const schema = buildFixtureSchema()
    const entries: SubmitAnswerEntry[] = [
      { code: '2.5', instanceId: 'default', value: 'Femenino' },
      { code: '99.9', instanceId: 'default', value: 'inyectado' },
    ]
    const result = validateSubmission(schema, entries, true)
    assert.equal(result.ok, false)
    if (result.ok) return
    assert.ok(result.errors.some((e) => e.startsWith('unknown_question:99.9')))
  })

  test('rechaza respuesta a una fila type=section', () => {
    const schema = buildFixtureSchema()
    const entries: SubmitAnswerEntry[] = [
      { code: '2.5', instanceId: 'default', value: 'Femenino' },
      { code: 'S4', instanceId: 'default', value: 'algo' },
    ]
    const result = validateSubmission(schema, entries, true)
    assert.equal(result.ok, false)
    if (result.ok) return
    assert.ok(result.errors.some((e) => e.startsWith('section_is_not_answerable:S4')))
  })

  test('rechaza valor de tipo incorrecto (string donde se espera array)', () => {
    const schema = buildFixtureSchema()
    const entries: SubmitAnswerEntry[] = [
      { code: '2.5', instanceId: 'default', value: 'Femenino' },
      { code: '4.1', instanceId: 'default', value: 'Hijo menor con discapacidad' as unknown as string[] },
    ]
    const result = validateSubmission(schema, entries, true)
    assert.equal(result.ok, false)
    if (result.ok) return
    assert.ok(result.errors.some((e) => e.includes('expected_array_value')))
  })

  test('reporta TODOS los errores encontrados, no solo el primero', () => {
    const schema = buildFixtureSchema()
    const entries: SubmitAnswerEntry[] = [
      { code: '99.9', instanceId: 'default', value: 'inyectado' },
      { code: 'S4', instanceId: 'default', value: 'algo' },
    ]
    const result = validateSubmission(schema, entries, true)
    assert.equal(result.ok, false)
    if (result.ok) return
    assert.ok(result.errors.length >= 2, `esperaba >=2 errores, hubo ${result.errors.length}`)
  })
})

describe('valores vacíos', () => {
  test('una entrada con valor vacío no se valida ni se persiste, pero no bloquea el submit', () => {
    const schema = buildFixtureSchema()
    const entries: SubmitAnswerEntry[] = [
      { code: '2.5', instanceId: 'default', value: 'Femenino' },
      { code: '3.1', instanceId: 'default', value: '' }, // vacío — se ignora, no se evalúa su visibilidad
    ]
    const result = validateSubmission(schema, entries, true)
    assert.equal(result.ok, true)
    if (!result.ok) return
    assert.equal(result.rows.some((r) => r.questionId === '3.1'), false)
  })
})
