// Tests Node puros (sin navegador) del motor de resolución de flujo v3.
// Ejecutar: node --test lib/form-engine.test.mts
//
// Extensión .mts (no .ts): así Node ejecuta el archivo como ESM nativo con
// soporte de TypeScript sin configuración adicional, y el glob "**/*.ts" del
// tsconfig.json del proyecto (usado por `next build`/`tsc`) no lo incluye —
// evita el conflicto entre "Node necesita extensión .ts explícita en los
// imports" y "tsc rechaza imports con extensión .ts sin allowImportingTsExtensions".
import { test, describe } from 'node:test'
import assert from 'node:assert/strict'
import {
  buildFormSchema,
  evaluateRule,
  resolveVisibility,
  resolveScreens,
  initialState,
  setAnswer,
  applyAnswer,
  isRequiredAndUnanswered,
  next,
  back,
} from './form-engine.ts'
import type { ConditionalRule, FormAnswers, Question } from './form-engine.types.ts'

// ── Fixture: réplica fiel de S3–S10 del seed v3 (supabase/seed.sql) ──────────
// S1/S2 no tienen conditional_rules y no aportan a estos tests; se omiten.

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 VINCULOS = [
  'Hijo menor sin discapacidad',
  'Hijo menor con discapacidad',
  'Hijo adulto con discapacidad',
  'Familiar menor con discapacidad',
  'Familiar mayor con discapacidad',
  'Padre/madre con movilidad reducida o 100% dependiente',
  'Otro / Especificar',
  'Ninguna de las anteriores / No tengo vinculación de cuidado',
] as const
const NINGUNA = VINCULOS[7]
const SOLO_NINO = VINCULOS[0]

const r_show_si_s3_1: ConditionalRule[] = [
  { if_question: '3.1', op: 'eq', value: 'Sí', action: 'show' },
]
const r_hide_ning: ConditionalRule[] = [
  { if_question: '4.1', op: 'contains', value: NINGUNA, action: 'hide' },
]
const r_hide_ning_sn: ConditionalRule[] = [
  { if_question: '4.1', op: 'contains', value: NINGUNA, action: 'hide' },
  { if_question: '4.1', op: 'eq_sole', value: SOLO_NINO, action: 'hide' },
]

function buildFixtureSchema() {
  const rows: Question[] = [
    // S3 — Prevalencia de discapacidad
    q({ code: 'S3', type: 'section', prompt: 'Prevalencia de discapacidad', position: 30 }),
    q({ code: '3.1', type: 'single_choice', prompt: '¿Tenés discapacidad?', position: 31 }),
    q({ code: '3.2', type: 'multiple_choice', prompt: 'Tipo', position: 32, conditional_rules: r_show_si_s3_1 }),
    q({ code: '3.3', type: 'multiple_choice', prompt: 'Dispositivos', position: 33, conditional_rules: r_show_si_s3_1 }),
    q({ code: '3.4', type: 'single_choice', prompt: 'Certificado', position: 34, conditional_rules: r_show_si_s3_1 }),
    q({ code: '3.5', type: 'multiple_choice', prompt: 'Impacto', position: 35, conditional_rules: r_show_si_s3_1 }),

    // S4 — Vinculación familiar (4.1 generador; 4.2/4.3 instancia)
    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', conditional_rules: r_hide_ning_sn }),
    q({ code: '4.3', type: 'multiple_choice', prompt: 'Dispositivos familiar', position: 43, instance_of: '4.1', conditional_rules: r_hide_ning_sn }),

    // S5 — Responsabilidad y cuidado
    q({ code: 'S5', type: 'section', prompt: 'Responsabilidad y cuidado', position: 50, conditional_rules: r_hide_ning }),
    q({ code: '5.1', type: 'multiple_choice', prompt: 'Rol', position: 51, conditional_rules: r_hide_ning_sn }),
    q({ code: '5.2', type: 'multiple_choice', prompt: 'Momento', position: 52, conditional_rules: r_hide_ning_sn }),
    q({ code: '5.3', type: 'multiple_choice', prompt: 'Quién cuida en horario laboral', position: 53, conditional_rules: r_hide_ning }),
    q({ code: '5.4', type: 'single_choice', prompt: 'Desde cuándo', position: 54, conditional_rules: r_hide_ning_sn }),

    // S6 — Intensidad del cuidado
    q({ code: 'S6', type: 'section', prompt: 'Intensidad del cuidado', position: 60, conditional_rules: r_hide_ning_sn }),
    q({ code: '6.1', type: 'single_choice', prompt: 'Días/semana', position: 61, conditional_rules: r_hide_ning_sn }),
    q({ code: '6.2', type: 'single_choice', prompt: 'Horas/día', position: 62, conditional_rules: r_hide_ning_sn }),
    q({ code: '6.3', type: 'multiple_choice', prompt: 'Momentos', position: 63, conditional_rules: r_hide_ning_sn }),
    q({ code: '6.4', type: 'single_choice', prompt: 'Disponibilidad', position: 64, conditional_rules: r_hide_ning_sn }),

    // S7 — Impacto en el trabajo
    q({ code: 'S7', type: 'section', prompt: 'Impacto en el trabajo', position: 70, conditional_rules: r_hide_ning_sn }),
    q({ code: '7.1', type: 'single_choice', prompt: 'Descanso', position: 71, conditional_rules: r_hide_ning_sn }),
    q({ code: '7.2', type: 'single_choice', prompt: 'Concentración', position: 72, conditional_rules: r_hide_ning_sn }),
    q({ code: '7.3', type: 'single_choice', prompt: 'Ausencias', position: 73, conditional_rules: r_hide_ning_sn }),
    q({ code: '7.4', type: 'multiple_choice', prompt: 'Aspectos afectados', position: 74, conditional_rules: r_hide_ning_sn }),
    q({ code: '7.5', type: 'multiple_choice', prompt: 'Consecuencias laborales', position: 75, conditional_rules: r_hide_ning_sn }),
    q({ code: '7.6', type: 'single_choice', prompt: 'Probabilidad de dejar', position: 76, conditional_rules: r_hide_ning_sn }),

    // S8 — Impacto económico
    q({ code: 'S8', type: 'section', prompt: 'Impacto económico', position: 80, conditional_rules: r_hide_ning }),
    q({ code: '8.1', type: 'single_choice', prompt: 'Gasto mensual', position: 81, conditional_rules: r_hide_ning_sn }),
    q({ code: '8.2', type: 'multiple_choice', prompt: 'Conceptos', position: 82, conditional_rules: r_hide_ning_sn }),
    q({ code: '8.3', type: 'multiple_choice', prompt: 'Cómo sostiene', position: 83, conditional_rules: r_hide_ning_sn }),
    q({ code: '8.4', type: 'single_choice', prompt: 'Franja salarial', position: 84, is_sensitive: true, conditional_rules: r_hide_ning }),

    // S9 — Conciencia organizacional
    q({ code: 'S9', type: 'section', prompt: 'Conciencia organizacional', position: 90, conditional_rules: r_hide_ning }),
    q({ code: '9.1', type: 'single_choice', prompt: 'Conciencia empresa', position: 91, conditional_rules: r_hide_ning }),
    q({ code: '9.2', type: 'multiple_choice', prompt: 'Apoyos', position: 92, conditional_rules: r_hide_ning_sn }),
    q({ code: '9.3', type: 'single_choice', prompt: 'Grupo de apoyo', position: 93, conditional_rules: r_hide_ning_sn }),
    q({ code: '9.4', type: 'single_choice', prompt: 'Voluntariado', position: 94, conditional_rules: r_hide_ning }),

    // S10 — Comentarios adicionales
    q({ code: 'S10', type: 'section', prompt: 'Comentarios adicionales', position: 100 }),
    q({ code: '10.1', type: 'text_long', prompt: 'Comentario', position: 101 }),
  ]
  return buildFormSchema(rows)
}

function codesOf(screens: ReturnType<typeof resolveScreens>): string[] {
  return screens.filter((s) => s.kind !== 'end').map((s) => s.question!.code)
}

// ── 1. Recorrido completo ────────────────────────────────────────────────
test('recorrido completo: todos los vínculos (2-7) muestran S5-S9 enteras + 12 pantallas de instancia', () => {
  const schema = buildFixtureSchema()
  const answers: FormAnswers = {
    '4.1:default': VINCULOS.slice(1, 7), // opciones 2..7, sin soloNino ni Ninguna
  }
  const screens = resolveScreens(schema, answers)
  const codes = codesOf(screens)

  for (const expected of ['S5', '5.1', '5.2', '5.3', '5.4', 'S6', '6.1', '6.4', 'S7', '7.1', '7.6', 'S8', '8.1', '8.4', 'S9', '9.1', '9.4']) {
    assert.ok(codes.includes(expected), `falta ${expected} en recorrido completo`)
  }

  const instanceScreens = screens.filter((s) => s.instanceId !== null)
  assert.equal(instanceScreens.length, 12, '6 familiares x 2 preguntas (4.2+4.3) = 12 pantallas')
})

// ── 2. Solo niño (eq_sole) ───────────────────────────────────────────────
test('solo niño sin discapacidad (eq_sole): recorrido liviano 5.3, 8.4, 9.1, 9.4', () => {
  const schema = buildFixtureSchema()
  const answers: FormAnswers = { '4.1:default': [SOLO_NINO] }
  const screens = resolveScreens(schema, answers)
  const codes = codesOf(screens)

  // visibles: S5(header)+5.3, S8(header)+8.4, S9(header)+9.1+9.4
  for (const expected of ['S5', '5.3', 'S8', '8.4', 'S9', '9.1', '9.4']) {
    assert.ok(codes.includes(expected), `falta ${expected} en soloNino`)
  }
  // ocultas
  for (const hidden of ['5.1', '5.2', '5.4', 'S6', '6.1', 'S7', '7.1', '8.1', '8.2', '8.3', '9.2', '9.3']) {
    assert.ok(!codes.includes(hidden), `${hidden} no debería aparecer en soloNino`)
  }
  // 4.2/4.3 no se muestran (la única instancia generada queda oculta por r_hide_ning_sn)
  assert.equal(screens.filter((s) => s.instanceId !== null).length, 0)
})

// ── 3. Ninguna de las anteriores (contains) ──────────────────────────────
test('"Ninguna de las anteriores" (contains): salta directo a S10, incluso combinada con otra opción', () => {
  const schema = buildFixtureSchema()
  const answers: FormAnswers = { '4.1:default': [VINCULOS[2], NINGUNA] } // marcada junto con otra
  const screens = resolveScreens(schema, answers)
  const codes = codesOf(screens)

  for (const hidden of ['S5', 'S6', 'S7', 'S8', '8.4', 'S9', '9.1', '9.4']) {
    assert.ok(!codes.includes(hidden), `${hidden} no debería aparecer con Ninguna marcada`)
  }
  assert.ok(codes.includes('S10') && codes.includes('10.1'))
  // el índice de S4 (4.1) debe ser inmediatamente anterior a S10 en la lista
  const idxS4 = codes.indexOf('4.1')
  const idxS10 = codes.indexOf('S10')
  assert.equal(idxS10, idxS4 + 1, 'S10 debe seguir inmediatamente a 4.1')
})

// ── 4. 3.1='No' oculta 3.2-3.5 ────────────────────────────────────────────
test("3.1='No' oculta 3.2-3.5; 3.1='Sí' las muestra", () => {
  const schema = buildFixtureSchema()

  const codesNo = codesOf(resolveScreens(schema, { '3.1:default': 'No' }))
  for (const hidden of ['3.2', '3.3', '3.4', '3.5']) assert.ok(!codesNo.includes(hidden))
  assert.ok(codesNo.includes('3.1'))

  const codesSi = codesOf(resolveScreens(schema, { '3.1:default': 'Sí' }))
  for (const shown of ['3.2', '3.3', '3.4', '3.5']) assert.ok(codesSi.includes(shown))
})

// ── 5. 3 familiares → 6 pantallas de instancia en orden por familiar ─────
test('3 familiares (seleccionados fuera de orden) generan 6 pantallas en orden de esquema, no de selección', () => {
  const schema = buildFixtureSchema()
  // Selección deliberadamente "fuera de orden" respecto a VINCULOS: opción 6, luego 2, luego 4.
  const answers: FormAnswers = { '4.1:default': [VINCULOS[5], VINCULOS[1], VINCULOS[3]] }
  const screens = resolveScreens(schema, answers)
  const instanceScreens = screens.filter((s) => s.instanceId !== null)

  assert.equal(instanceScreens.length, 6)
  // Orden esperado por posición FIJA en options (índices 1, 3, 5 → familiar_2, familiar_4, familiar_6)
  assert.deepEqual(
    instanceScreens.map((s) => s.id),
    [
      'question:4.2:familiar_2', 'question:4.3:familiar_2',
      'question:4.2:familiar_4', 'question:4.3:familiar_4',
      'question:4.2:familiar_6', 'question:4.3:familiar_6',
    ]
  )
})

test('IDs de instancia son estables: agregar un vínculo nuevo no reindexa los existentes', () => {
  const schema = buildFixtureSchema()
  const before = resolveScreens(schema, { '4.1:default': [VINCULOS[1], VINCULOS[4]] })
    .filter((s) => s.instanceId !== null)
    .map((s) => s.id)
  assert.deepEqual(before, [
    'question:4.2:familiar_2', 'question:4.3:familiar_2',
    'question:4.2:familiar_5', 'question:4.3:familiar_5',
  ])

  // Se agrega un vínculo ANTERIOR en el orden de opciones (índice 2) — los ids de
  // familiar_2 y familiar_5 deben seguir siendo exactamente los mismos.
  const after = resolveScreens(schema, { '4.1:default': [VINCULOS[1], VINCULOS[2], VINCULOS[4]] })
    .filter((s) => s.instanceId !== null)
    .map((s) => s.id)
  assert.deepEqual(after, [
    'question:4.2:familiar_2', 'question:4.3:familiar_2',
    'question:4.2:familiar_3', 'question:4.3:familiar_3',
    'question:4.2:familiar_5', 'question:4.3:familiar_5',
  ])
})

// ── 6. Navegación next/back coherente con instancias ─────────────────────
describe('navegación next/back', () => {
  test('next/back recorren secuencialmente section→pregunta→instancias→fin', () => {
    const schema = buildFixtureSchema()
    let state = initialState(schema)
    assert.equal(state.currentScreenId, 'section:S3')

    state = next(state, schema)
    assert.equal(state.currentScreenId, 'question:3.1')
    state = setAnswer(state, '3.1', 'No')
    // 3.2-3.5 ocultas → next desde 3.1 debe saltar directo a S4
    state = next(state, schema)
    assert.equal(state.currentScreenId, 'section:S4')
  })

  test('next/back son estables cuando cambian las instancias activas', () => {
    const schema = buildFixtureSchema()
    let state = initialState(schema)
    state = setAnswer(state, '4.1', [VINCULOS[1], VINCULOS[4]]) // familiar_2, familiar_5
    state.currentScreenId = 'question:4.1'

    // Avanza a la primera pregunta de instancia
    state = next(state, schema)
    assert.equal(state.currentScreenId, 'question:4.2:familiar_2')
    state = next(state, schema)
    assert.equal(state.currentScreenId, 'question:4.3:familiar_2')

    // Mientras está parado en 4.3:familiar_2, el usuario (vía 4D) agrega un
    // vínculo anterior en el esquema (índice 2) sin moverse de pantalla.
    state = setAnswer(state, '4.1', [VINCULOS[1], VINCULOS[2], VINCULOS[4]])

    // El ID actual sigue existiendo y sigue siendo válido — no se desincroniza.
    const next1 = next(state, schema)
    assert.equal(next1.currentScreenId, 'question:4.2:familiar_3', 'avanza al nuevo familiar_3 insertado en medio')

    const back1 = back(state, schema)
    assert.equal(back1.currentScreenId, 'question:4.2:familiar_2', 'retrocede a familiar_2, sin verse afectado por el nuevo familiar_3')
  })

  test('back() desde S10 tras "Ninguna" vuelve a 4.1, saltando las secciones ocultas', () => {
    const schema = buildFixtureSchema()
    let state = initialState(schema)
    state = setAnswer(state, '4.1', [NINGUNA])
    state.currentScreenId = 'section:S10'

    state = back(state, schema)
    assert.equal(state.currentScreenId, 'question:4.1')
  })
})

// ── Operadores: cobertura unitaria directa (vocabulario completo) ───────
describe('evaluateRule — los 5 operadores', () => {
  const answers: FormAnswers = {
    '3.1:default': 'Sí',
    '4.1:default': ['Hijo menor con discapacidad', 'Otro / Especificar'],
    '2.6:default': 'Jefatura',
  }

  test('eq', () => {
    assert.equal(evaluateRule({ if_question: '3.1', op: 'eq', value: 'Sí', action: 'show' }, answers), true)
    assert.equal(evaluateRule({ if_question: '3.1', op: 'eq', value: 'No', action: 'show' }, answers), false)
  })

  test('eq_any', () => {
    const rule: ConditionalRule = { if_question: ['3.1', '2.6'], op: 'eq_any', value: 'Jefatura', action: 'show' }
    assert.equal(evaluateRule(rule, answers), true)
  })

  test('not_in', () => {
    const rule: ConditionalRule = { if_question: '2.6', op: 'not_in', value: ['Directivo', 'Gerencial'], action: 'hide' }
    assert.equal(evaluateRule(rule, answers), true)
    const rule2: ConditionalRule = { if_question: '2.6', op: 'not_in', value: ['Jefatura'], action: 'hide' }
    assert.equal(evaluateRule(rule2, answers), false)
  })

  test('contains', () => {
    const rule: ConditionalRule = { if_question: '4.1', op: 'contains', value: 'Otro / Especificar', action: 'hide' }
    assert.equal(evaluateRule(rule, answers), true)
  })

  test('eq_sole', () => {
    const rule: ConditionalRule = { if_question: '4.1', op: 'eq_sole', value: 'Otro / Especificar', action: 'hide' }
    assert.equal(evaluateRule(rule, answers), false) // hay 2 elementos, no es el único
  })
})

// ── Semántica show/hide combinados ───────────────────────────────────────
describe('resolveVisibility — combinación show/hide', () => {
  test('hide gana siempre sobre show, aunque show también coincida', () => {
    const mixed: ConditionalRule[] = [
      { if_question: '3.1', op: 'eq', value: 'Sí', action: 'show' },
      { if_question: '4.1', op: 'contains', value: NINGUNA, action: 'hide' },
    ]
    const question = q({ code: 'X', type: 'single_choice', prompt: 'x', position: 1, conditional_rules: mixed })
    const answers: FormAnswers = { '3.1:default': 'Sí', '4.1:default': [NINGUNA] }
    assert.equal(resolveVisibility(question, answers), false)
  })

  test('sin reglas hide, show gana si coincide', () => {
    const onlyShow: ConditionalRule[] = [{ if_question: '3.1', op: 'eq', value: 'Sí', action: 'show' }]
    const question = q({ code: 'X', type: 'single_choice', prompt: 'x', position: 1, conditional_rules: onlyShow })
    assert.equal(resolveVisibility(question, { '3.1:default': 'Sí' }), true)
    assert.equal(resolveVisibility(question, { '3.1:default': 'No' }), false)
  })
})

// ── applyAnswer — integridad de datos al remover una instancia ──────────
// Esta lógica vivía en wizard-form.tsx (handleChange) hasta ahora; se movió
// al motor para que sea testeable como módulo puro (ver conversación 4E).
describe('applyAnswer — limpieza de respuestas al remover una instancia', () => {
  test('quitar un vínculo de 4.1 borra las respuestas de esa instancia y deja intactas las del resto', () => {
    const schema = buildFixtureSchema()
    let state = { answers: {} as FormAnswers, currentScreenId: 'question:4.1' }

    // Selecciona 2 vínculos → familiar_2 (índice 1) y familiar_5 (índice 4)
    state = applyAnswer(state, schema, '4.1', [VINCULOS[1], VINCULOS[4]])

    // Responde 4.2/4.3 para AMBAS instancias
    state = applyAnswer(state, schema, '4.2', ['Visual'], 'familiar_2')
    state = applyAnswer(state, schema, '4.3', ['Bastón blanco'], 'familiar_2')
    state = applyAnswer(state, schema, '4.2', ['Auditiva'], 'familiar_5')
    state = applyAnswer(state, schema, '4.3', ['Audífonos'], 'familiar_5')

    assert.deepEqual(state.answers['4.2:familiar_2'], ['Visual'])
    assert.deepEqual(state.answers['4.3:familiar_2'], ['Bastón blanco'])
    assert.deepEqual(state.answers['4.2:familiar_5'], ['Auditiva'])
    assert.deepEqual(state.answers['4.3:familiar_5'], ['Audífonos'])

    // Quita el vínculo de familiar_2 — deja solo familiar_5 en 4.1
    state = applyAnswer(state, schema, '4.1', [VINCULOS[4]])

    // Las respuestas de la instancia removida desaparecen del estado
    assert.equal('4.2:familiar_2' in state.answers, false)
    assert.equal('4.3:familiar_2' in state.answers, false)

    // Las del familiar que queda permanecen intactas, sin reindexarse
    assert.deepEqual(state.answers['4.2:familiar_5'], ['Auditiva'])
    assert.deepEqual(state.answers['4.3:familiar_5'], ['Audífonos'])
  })

  test('aplicar la misma respuesta sin cambiar visibilidad no borra nada (caso sin disappeared)', () => {
    const schema = buildFixtureSchema()
    let state = { answers: {} as FormAnswers, currentScreenId: 'question:3.1' }
    state = applyAnswer(state, schema, '3.1', 'Sí')
    state = applyAnswer(state, schema, '3.2', ['Física / motora'])
    state = applyAnswer(state, schema, '3.1', 'Sí') // mismo valor, no cambia nada visible
    assert.deepEqual(state.answers['3.2:default'], ['Física / motora'])
  })
})

// ── isRequiredAndUnanswered — incluye preguntas de instancia ─────────────
// El seed v3 actual no marca ninguna pregunta de instancia (4.2/4.3) como
// required — este test usa un esquema sintético mínimo para cubrir el
// camino igual, ya que el código no debe asumir que nunca va a pasar.
function buildRequiredInstanceSchema() {
  const rows: Question[] = [
    q({
      code: 'G1', type: 'multiple_choice', prompt: 'Generador', position: 1,
      instance_generator: true,
      options: [{ value: 'a', label: 'A' }, { value: 'b', label: 'B' }],
    }),
    q({ code: 'G1.1', type: 'text_short', prompt: 'Dato requerido por instancia', position: 2, instance_of: 'G1', required: true }),
  ]
  return buildFormSchema(rows)
}

describe('isRequiredAndUnanswered — cubre preguntas de instancia required', () => {
  test('cada instancia de una pregunta required se evalúa por separado', () => {
    const schema = buildRequiredInstanceSchema()
    const answers: FormAnswers = { 'G1:default': ['a', 'b'] }
    const screens = resolveScreens(schema, answers)

    const screenA = screens.find((s) => s.id === 'question:G1.1:familiar_1')
    const screenB = screens.find((s) => s.id === 'question:G1.1:familiar_2')
    assert.ok(screenA && screenB, 'deben existir 2 pantallas de instancia para G1.1')

    // Sin responder ninguna instancia → unanswered en ambas
    assert.equal(isRequiredAndUnanswered(screenA!, answers), true)
    assert.equal(isRequiredAndUnanswered(screenB!, answers), true)

    // Responder SOLO la instancia 1 → deja de exigirse esa, la otra sigue pendiente
    const partial = { ...answers, 'G1.1:familiar_1': 'dato' }
    assert.equal(isRequiredAndUnanswered(screenA!, partial), false)
    assert.equal(isRequiredAndUnanswered(screenB!, partial), true)
  })

  test('preguntas no-required nunca bloquean, tengan o no respuesta', () => {
    const schema = buildFixtureSchema()
    const screens = resolveScreens(schema, {})
    const screen = screens.find((s) => s.id === 'question:3.1')
    assert.ok(screen)
    assert.equal(isRequiredAndUnanswered(screen!, {}), false)
  })
})
