/**
 * Test integrador del flujo ROI completo — Sprint 1 Etapa 3.
 *
 * Para cada sample BPMN verifica que el pipeline:
 *   parseBpmnXml → runSimulation (dual) → calculateRoi
 * produce KPIs numéricos válidos y detecta las condiciones de transparencia
 * metodológica correctamente.
 *
 * Los 4 casos:
 *   A) simple-linear SIN automatizables → automatableIds.size = 0
 *   B) medium-with-gateways, 3 automatable → KPIs positivos, payback razonable
 *   C) medium-with-gateways, 1 automatable en ceros + investment=0 → transparencia doble
 *   D) complex-with-loop → warnings preservados en ambos escenarios
 */

import { describe, it, expect } from 'vitest'
import { readFileSync } from 'fs'
import { resolve } from 'path'
import { v4 as uuidv4 } from 'uuid'
import { parseBpmnXml, extractActivityElements, extractGatewayElements } from '@/domain/bpmn-parser'
import { runSimulation } from '@/domain/simulation'
import { calculateRoi } from '@/domain/roi'
import { formatCurrency, formatPercent } from '@/lib/format'
import { bpmnTypeToGatewayType } from '@/domain/types'
import type { Activity, GatewayConfig, SimulationInput } from '@/domain/types'

// ─── Helpers ──────────────────────────────────────────────────────────────────

function loadBpmn(name: string): string {
  return readFileSync(resolve(__dirname, '../../public/sample-processes', name), 'utf-8')
}

function buildSimInput(
  xml: string,
  directCost: number,
  automatableIds: Set<string>,
  automatedCost: number,
  automatedTime: number,
): SimulationInput {
  const graph = parseBpmnXml(xml)
  const actEls = extractActivityElements(graph)
  const gwEls = extractGatewayElements(graph)
  const processId = 'roi-test'

  const activities = new Map<string, Activity>(
    actEls.map((el) => [el.bpmnElementId, {
      id: uuidv4(), processId, bpmnElementId: el.bpmnElementId,
      name: el.name, type: el.type,
      directCostFixed: directCost,
      executionTimeMinutes: 60,
      assignedResources: [],
      automatable: automatableIds.has(el.bpmnElementId),
      automatedCostFixed: automatableIds.has(el.bpmnElementId) ? automatedCost : 0,
      automatedTimeMinutes: automatableIds.has(el.bpmnElementId) ? automatedTime : 0,
      automatedAssignedResources: [],
    }])
  )

  const gateways = new Map<string, GatewayConfig>(
    gwEls.map((el) => {
      const gwType = bpmnTypeToGatewayType(el.gatewayType as any)
      const n = el.outgoing.length
      return [el.bpmnElementId, {
        id: uuidv4(), processId, bpmnElementId: el.bpmnElementId,
        gatewayType: gwType,
        branches: el.outgoing.map((flowId) => ({
          flowId, targetElementId: graph.flows.get(flowId)?.targetRef ?? '',
          probability: gwType === 'parallel' ? 1.0 : parseFloat((1 / n).toFixed(4)),
        })),
      }]
    })
  )

  return {
    processXml: xml,
    activities,
    gateways,
    resources: new Map(),
    globalSettings: { currency: 'USD', overheadPercentage: 0.2 },
  }
}

// Volumetría estándar para todos los tests (la especificada en el brief)
const VOL = { annualFrequency: 12_000, analysisHorizonYears: 3, automationInvestment: 50_000 }

// ─── CASO A: simple-linear SIN automatizables ─────────────────────────────────

describe('Caso A — simple-linear SIN automatizables', () => {
  const xml = loadBpmn('simple-linear.bpmn')
  const noAutomatable = new Set<string>()
  const input = buildSimInput(xml, 200, noAutomatable, 0, 0)

  const resultActual = runSimulation({ ...input, scenario: 'actual' })
  const resultAutomated = runSimulation({ ...input, scenario: 'automated' })

  it('simple-linear tiene 5 actividades', () => {
    const graph = parseBpmnXml(xml)
    const actEls = extractActivityElements(graph)
    expect(actEls).toHaveLength(5)
  })

  it('automatableIds.size === 0: ninguna actividad marcada como automatizable', () => {
    const graph = parseBpmnXml(xml)
    const actEls = extractActivityElements(graph)
    const automatableIds = actEls.filter((el) => noAutomatable.has(el.bpmnElementId))
    expect(automatableIds).toHaveLength(0)
  })

  it('resultActual y resultAutomated tienen costos idénticos (sin automatizables)', () => {
    expect(resultActual.totalCost).toBeCloseTo(resultAutomated.totalCost, 4)
  })

  it('ROI con costos idénticos: savingsPerExecution = 0, annualSavings = 0', () => {
    const roi = calculateRoi({
      costPerExecutionActual: resultActual.totalCost,
      costPerExecutionAutomated: resultAutomated.totalCost,
      ...VOL,
    })
    expect(roi.savingsPerExecution).toBeCloseTo(0, 4)
    expect(roi.annualSavings).toBeCloseTo(0, 2)
  })

  it('con ahorro cero: paybackMonths = Infinity', () => {
    const roi = calculateRoi({
      costPerExecutionActual: resultActual.totalCost,
      costPerExecutionAutomated: resultAutomated.totalCost,
      ...VOL,
    })
    expect(roi.paybackMonths).toBe(Infinity)
  })

  it('con ahorro cero: breaksEvenInHorizon = false', () => {
    const roi = calculateRoi({
      costPerExecutionActual: resultActual.totalCost,
      costPerExecutionAutomated: resultAutomated.totalCost,
      ...VOL,
    })
    expect(roi.breaksEvenInHorizon).toBe(false)
  })
})

// ─── CASO B: medium-with-gateways con 3 automatizables ───────────────────────

describe('Caso B — medium-with-gateways, 3 automatizables, valores plausibles', () => {
  const xml = loadBpmn('medium-with-gateways.bpmn')
  // task_recv (prob≈1.0), task_score (prob≈0.5), task_analisis (prob≈0.5 post-parallel)
  const automatable3 = new Set(['task_recv', 'task_score', 'task_analisis'])
  const input = buildSimInput(xml, 200, automatable3, 20, 5)

  const resultActual = runSimulation({ ...input, scenario: 'actual' })
  const resultAutomated = runSimulation({ ...input, scenario: 'automated' })
  const roi = calculateRoi({
    costPerExecutionActual: resultActual.totalCost,
    costPerExecutionAutomated: resultAutomated.totalCost,
    ...VOL,
  })

  it('reporta los KPIs reales del Caso B para inspección manual', () => {
    // Este test siempre pasa — su propósito es mostrar valores en la consola
    console.log('\n╔══════════════════════════════════════════════════════════╗')
    console.log('║  CASO B: medium-with-gateways — KPIs del sistema        ║')
    console.log('╠══════════════════════════════════════════════════════════╣')
    console.log(`║  Costo actual por ejecución:  ${formatCurrency(resultActual.totalCost, 'USD').padEnd(26)} ║`)
    console.log(`║  Costo automático por ejec.:  ${formatCurrency(resultAutomated.totalCost, 'USD').padEnd(26)} ║`)
    console.log('╠══════════════════════════════════════════════════════════╣')
    console.log(`║  Ahorro por ejecución:   ${formatCurrency(roi.savingsPerExecution, 'USD').padEnd(30)} ║`)
    console.log(`║  Reducción %:            ${formatPercent(roi.savingsPerExecutionPercent).padEnd(30)} ║`)
    console.log(`║  Ahorro anual:           ${formatCurrency(roi.annualSavings, 'USD').padEnd(30)} ║`)
    console.log(`║  Payback:                ${roi.paybackMonths.toFixed(1)} meses`.padEnd(57) + '║')
    console.log(`║  Acumulado a 3 años:     ${formatCurrency(roi.cumulativeSavingsHorizon, 'USD').padEnd(30)} ║`)
    console.log(`║  ROI anualizado:         ${formatPercent(roi.roiAnnualPercent).padEnd(30)} ║`)
    console.log('╚══════════════════════════════════════════════════════════╝\n')
    expect(true).toBe(true)
  })

  it('dump de costos por actividad — reconciliación aritmética completa', () => {
    const autoByElem  = new Map(resultAutomated.perActivity.map((a) => [a.bpmnElementId, a]))

    // Construcción de la tabla en orden de procesamiento topológico
    const all = resultActual.perActivity
      .slice()
      .sort((a, b) => b.executionProbability - a.executionProbability || a.activityName.localeCompare(b.activityName))

    const AUTOMATABLE = new Set(['task_recv', 'task_score', 'task_analisis'])

    let directActualSum = 0
    let directAutoSum = 0

    console.log('\n┌────────────────────────────────┬──────┬──────────────┬──────────────┐')
    console.log('│ bpmnElementId                  │ prob │ directo act. │ directo auto │')
    console.log('├────────────────────────────────┼──────┼──────────────┼──────────────┤')
    for (const act of all) {
      const aAuto = autoByElem.get(act.bpmnElementId)
      const prob  = act.executionProbability
      const dAct  = act.expectedDirectCost
      const dAuto = aAuto?.expectedDirectCost ?? 0
      const auto  = AUTOMATABLE.has(act.bpmnElementId) ? '★' : ' '
      directActualSum += dAct
      directAutoSum   += dAuto
      const idPadded   = act.bpmnElementId.padEnd(30)
      const probStr    = prob.toFixed(2).padStart(4)
      const actStr     = dAct.toFixed(2).padStart(12)
      const autoStr    = dAuto.toFixed(2).padStart(12)
      console.log(`│ ${auto}${idPadded} │ ${probStr} │ ${actStr} │ ${autoStr} │`)
    }
    console.log('├────────────────────────────────┼──────┼──────────────┼──────────────┤')
    console.log(`│ SUMA DIRECTA                   │      │ ${directActualSum.toFixed(2).padStart(12)} │ ${directAutoSum.toFixed(2).padStart(12)} │`)
    console.log(`│ + overhead 20%                 │      │ ${(directActualSum * 1.2).toFixed(2).padStart(12)} │ ${(directAutoSum * 1.2).toFixed(2).padStart(12)} │`)
    console.log('└────────────────────────────────┴──────┴──────────────┴──────────────┘\n')

    // Verificaciones aritméticas exactas
    // sum probs de actividades: 1+1+0.5+0.5+0.5+0.5+1+0.5+0.5+0.5+1 = 7.5
    const sumProbs = all.reduce((s, a) => s + a.executionProbability, 0)
    expect(sumProbs).toBeCloseTo(7.5, 4)

    // Directo actual = 7.5 × $200 = $1.500
    expect(directActualSum).toBeCloseTo(1500, 2)

    // Total con overhead = $1.800
    expect(resultActual.totalCost).toBeCloseTo(1800, 2)

    // Directo automatizado:
    // task_recv(1×$20)+task_valid(1×$200)+task_score(0.5×$20)+task_pedir_docs(0.5×$200)
    // +task_bureau(0.5×$200)+task_empleador(0.5×$200)+task_analisis(1×$20)
    // +task_aprobar(0.5×$200)+task_rechazar(0.5×$200)+task_desembolso(0.5×$200)+task_archivo(1×$200)
    // = $20+$200+$10+$100+$100+$100+$20+$100+$100+$100+$200 = $1.050
    expect(directAutoSum).toBeCloseTo(1050, 2)

    // Total automatizado con overhead = $1.260
    expect(resultAutomated.totalCost).toBeCloseTo(1260, 2)
  })

  it('costAutomated < costActual (hay ahorro)', () => {
    expect(resultAutomated.totalCost).toBeLessThan(resultActual.totalCost)
  })

  it('savingsPerExecution > 0 (ahorro positivo por ejecución)', () => {
    expect(roi.savingsPerExecution).toBeGreaterThan(0)
  })

  it('annualSavings > 0 (ahorro anual positivo)', () => {
    expect(roi.annualSavings).toBeGreaterThan(0)
  })

  it('paybackMonths es finito y positivo', () => {
    expect(Number.isFinite(roi.paybackMonths)).toBe(true)
    expect(roi.paybackMonths).toBeGreaterThan(0)
  })

  it('paybackMonths < 24 (recupera la inversión en menos de 2 años con datos plausibles)', () => {
    expect(roi.paybackMonths).toBeLessThan(24)
  })

  it('ROI anualizado > 100% (inversión se multiplica en el horizonte)', () => {
    expect(roi.roiAnnualPercent).toBeGreaterThan(100)
  })

  it('breaksEvenInHorizon = true (se recupera dentro de 3 años)', () => {
    expect(roi.breaksEvenInHorizon).toBe(true)
  })

  it('ningún KPI es NaN', () => {
    expect(Number.isNaN(roi.savingsPerExecution)).toBe(false)
    expect(Number.isNaN(roi.annualSavings)).toBe(false)
    expect(Number.isNaN(roi.cumulativeSavingsHorizon)).toBe(false)
    expect(Number.isNaN(roi.roiAnnualPercent)).toBe(false)
    expect(Number.isNaN(roi.paybackMonths)).toBe(false)
  })

  it('tabla de comparación: task_recv tiene ahorro diferente de cero', () => {
    const actualRecv = resultActual.perActivity.find((a) => a.bpmnElementId === 'task_recv')
    const autoRecv = resultAutomated.perActivity.find((a) => a.bpmnElementId === 'task_recv')
    expect(actualRecv).toBeDefined()
    expect(autoRecv).toBeDefined()
    expect(autoRecv!.expectedDirectCost).toBeLessThan(actualRecv!.expectedDirectCost)
  })

  it('tabla de comparación: actividades no automatable tienen costos idénticos en ambos escenarios', () => {
    const nonAutomatableIds = ['task_valid', 'task_bureau', 'task_empleador', 'task_archivo']
    for (const id of nonAutomatableIds) {
      const actual = resultActual.perActivity.find((a) => a.bpmnElementId === id)
      const auto = resultAutomated.perActivity.find((a) => a.bpmnElementId === id)
      if (!actual || !auto) continue
      expect(auto.expectedDirectCost).toBeCloseTo(actual.expectedDirectCost, 4)
    }
  })

  it('probabilidades de ejecución idénticas entre escenarios (el grafo no cambia)', () => {
    for (const actActual of resultActual.perActivity) {
      const actAuto = resultAutomated.perActivity.find(
        (a) => a.bpmnElementId === actActual.bpmnElementId
      )
      expect(actAuto).toBeDefined()
      expect(actAuto!.executionProbability).toBeCloseTo(actActual.executionProbability, 5)
    }
  })
})

// ─── CASO C: transparencia metodológica (ambas condiciones) ──────────────────

describe('Caso C — medium-with-gateways, 1 automatable en ceros + investment=0', () => {
  const xml = loadBpmn('medium-with-gateways.bpmn')
  const graph = parseBpmnXml(xml)
  const actEls = extractActivityElements(graph)
  const gwEls = extractGatewayElements(graph)

  // Construir actividades: task_valid marcada como automatable pero con costo=0 y tiempo=0
  const activities = new Map<string, Activity>(
    actEls.map((el) => [el.bpmnElementId, {
      id: uuidv4(), processId: 'c-test', bpmnElementId: el.bpmnElementId,
      name: el.name, type: el.type,
      directCostFixed: 200, executionTimeMinutes: 60, assignedResources: [],
      automatable: el.bpmnElementId === 'task_valid',
      automatedCostFixed: 0,   // cero — trigger de transparencia
      automatedTimeMinutes: 0, // cero — trigger de transparencia
      automatedAssignedResources: [],
    }])
  )

  const gateways = new Map<string, GatewayConfig>(
    gwEls.map((el) => {
      const gwType = bpmnTypeToGatewayType(el.gatewayType as any)
      const n = el.outgoing.length
      return [el.bpmnElementId, {
        id: uuidv4(), processId: 'c-test', bpmnElementId: el.bpmnElementId,
        gatewayType: gwType,
        branches: el.outgoing.map((flowId) => ({
          flowId, targetElementId: graph.flows.get(flowId)?.targetRef ?? '',
          probability: gwType === 'parallel' ? 1.0 : parseFloat((1 / n).toFixed(4)),
        })),
      }]
    })
  )

  const input: SimulationInput = {
    processXml: xml, activities, gateways, resources: new Map(),
    globalSettings: { currency: 'USD', overheadPercentage: 0.2 },
  }

  const resultActual = runSimulation({ ...input, scenario: 'actual' })
  const resultAutomated = runSimulation({ ...input, scenario: 'automated' })

  // investment=0 para el segundo trigger de transparencia
  const roi = calculateRoi({
    costPerExecutionActual: resultActual.totalCost,
    costPerExecutionAutomated: resultAutomated.totalCost,
    annualFrequency: 12_000,
    analysisHorizonYears: 3,
    automationInvestment: 0,  // ← trigger de transparencia
  })

  it('task_valid automatable con costo=0 → en escenario automatizado, task_valid contribuye $0 de costo directo', () => {
    const taskValidAuto = resultAutomated.perActivity.find((a) => a.bpmnElementId === 'task_valid')
    expect(taskValidAuto).toBeDefined()
    // automatable=true pero costo=0 → contribuye con $0 al directo automatizado
    expect(taskValidAuto!.expectedDirectCost).toBeCloseTo(0, 4)
  })

  it('TransparencySection Condición 1: hay 1 actividad automatable con costos en cero', () => {
    const zeroCostAutomatable = actEls.filter((el) => {
      const act = activities.get(el.bpmnElementId)
      return act?.automatable && act.automatedCostFixed === 0 && act.automatedTimeMinutes === 0
    })
    expect(zeroCostAutomatable).toHaveLength(1)
    expect(zeroCostAutomatable[0].bpmnElementId).toBe('task_valid')
  })

  it('TransparencySection Condición 2: investment=0 → paybackMonths=0 (inmediato)', () => {
    expect(roi.paybackMonths).toBe(0)
  })

  it('TransparencySection Condición 2: investment=0 → roiAnnualPercent=Infinity', () => {
    expect(roi.roiAnnualPercent).toBe(Infinity)
  })

  it('ambas condiciones de transparencia coexisten en el mismo proceso', () => {
    const zeroCostAutomatable = actEls.filter((el) => {
      const act = activities.get(el.bpmnElementId)
      return act?.automatable && act.automatedCostFixed === 0 && act.automatedTimeMinutes === 0
    })
    // Condición 1: al menos 1 automatable con costo=0
    expect(zeroCostAutomatable.length).toBeGreaterThan(0)
    // Condición 2: investment=0
    expect(roi.paybackMonths).toBe(0)
    expect(roi.roiAnnualPercent).toBe(Infinity)
  })

  it('el ahorro sigue siendo positivo aunque task_valid tenga costo=0 en automático (la reducción es real)', () => {
    // task_valid automatable con costo=0 reduce el total del escenario automatizado
    expect(resultAutomated.totalCost).toBeLessThan(resultActual.totalCost)
    expect(roi.savingsPerExecution).toBeGreaterThan(0)
  })
})

// ─── CASO D: complex-with-loop + warnings ─────────────────────────────────────

describe('Caso D — complex-with-loop, warnings preservados en ambos escenarios', () => {
  const xml = loadBpmn('complex-with-loop.bpmn')
  const automatable = new Set(['task_preparar', 'task_inspeccion'])
  const input = buildSimInput(xml, 200, automatable, 25, 5)

  const resultActual = runSimulation({ ...input, scenario: 'actual' })
  const resultAutomated = runSimulation({ ...input, scenario: 'automated' })

  it('el escenario actual produce warnings (loop detectado)', () => {
    expect(resultActual.warnings.length).toBeGreaterThan(0)
    const loopWarning = resultActual.warnings.some(
      (w) => w.toLowerCase().includes('loop') || w.toLowerCase().includes('ciclo') || w.toLowerCase().includes('visitad')
    )
    expect(loopWarning).toBe(true)
  })

  it('el escenario automatizado también tiene los mismos warnings de loop', () => {
    expect(resultAutomated.warnings.length).toBeGreaterThan(0)
    // La estructura del grafo no cambia entre escenarios → mismo warning
    expect(resultAutomated.warnings.length).toBe(resultActual.warnings.length)
  })

  it('el tab ROI (cambio de tab) NO silencia el warning — los datos de warnings coexisten', () => {
    // Este test valida que el SimulationResult preserva el array de warnings
    // independientemente del escenario. La UI del tab ROI no tiene WarningsBanner
    // (solo Actual y Automatizado lo tienen), pero los datos existen.
    expect(resultActual.warnings).toBeDefined()
    expect(Array.isArray(resultActual.warnings)).toBe(true)
    expect(resultAutomated.warnings).toBeDefined()
    expect(Array.isArray(resultAutomated.warnings)).toBe(true)
  })

  it('aún con loop, el totalCost no es NaN', () => {
    expect(Number.isNaN(resultActual.totalCost)).toBe(false)
    expect(Number.isNaN(resultAutomated.totalCost)).toBe(false)
  })

  it('aún con loop, la diferencia de costos entre escenarios es real y positiva', () => {
    expect(resultAutomated.totalCost).toBeLessThan(resultActual.totalCost)
  })

  it('aún con loop, calculateRoi no produce NaN ni errores', () => {
    const roi = calculateRoi({
      costPerExecutionActual: resultActual.totalCost,
      costPerExecutionAutomated: resultAutomated.totalCost,
      ...VOL,
    })
    expect(Number.isNaN(roi.savingsPerExecution)).toBe(false)
    expect(Number.isNaN(roi.annualSavings)).toBe(false)
    expect(Number.isNaN(roi.cumulativeSavingsHorizon)).toBe(false)
    expect(roi.savingsPerExecution).toBeGreaterThan(0)
  })
})
