/**
 * Tests permanentes del motor de simulación corriendo sobre los 3 sample BPMNs.
 * Protegen contra regresiones en runSimulation, la propagación de probabilidades
 * y la detección de loops.
 *
 * Costos dummy predecibles para que las aserciones sean deterministas.
 */

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 { bpmnTypeToGatewayType } from '@/domain/types'
import type { Activity, GatewayConfig, Resource, SimulationInput } from '@/domain/types'

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

// Construye un SimulationInput con costos fijos iguales para todas las actividades
function buildInput(
  xml: string,
  opts: {
    directCostFixed?: number
    executionTimeMinutes?: number
    overheadPercentage?: number
    gatewayProbsOverride?: Record<string, number[]>
  } = {}
): SimulationInput {
  const {
    directCostFixed = 100,
    executionTimeMinutes = 30,
    overheadPercentage = 0.2,
    gatewayProbsOverride = {},
  } = opts

  const processId = 'sim-test'
  const graph = parseBpmnXml(xml)
  const actEls = extractActivityElements(graph)
  const gwEls = extractGatewayElements(graph)

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

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

  return {
    processXml: xml,
    activities,
    gateways,
    resources: new Map<string, Resource>(),
    globalSettings: { currency: 'USD', overheadPercentage },
  }
}

// ─── simple-linear.bpmn ──────────────────────────────────────────────────────
describe('simulación / simple-linear.bpmn', () => {
  const xml = loadSample('simple-linear.bpmn')

  it('corre sin lanzar error con costos dummy', () => {
    expect(() => runSimulation(buildInput(xml))).not.toThrow()
  })

  it('no genera warnings (proceso sin loops ni gateways)', () => {
    const result = runSimulation(buildInput(xml))
    expect(result.warnings).toHaveLength(0)
  })

  it('costo total = 5 × 100 × 1.2 = 600 (5 tareas, prob=1, overhead=20%)', () => {
    const result = runSimulation(buildInput(xml, { directCostFixed: 100, overheadPercentage: 0.2 }))
    expect(result.totalDirectCost).toBe(500)
    expect(result.totalIndirectCost).toBeCloseTo(100)
    expect(result.totalCost).toBeCloseTo(600)
  })

  it('perActivity tiene 5 entradas, todas con executionProbability = 1', () => {
    const result = runSimulation(buildInput(xml))
    expect(result.perActivity).toHaveLength(5)
    result.perActivity.forEach((a) => {
      expect(a.executionProbability).toBeCloseTo(1.0)
    })
  })

  it('perActivity ordenado descendente por expectedTotalCost', () => {
    const result = runSimulation(buildInput(xml, { directCostFixed: 100 }))
    for (let i = 0; i < result.perActivity.length - 1; i++) {
      expect(result.perActivity[i].expectedTotalCost).toBeGreaterThanOrEqual(
        result.perActivity[i + 1].expectedTotalCost
      )
    }
  })

  it('percentOfTotal suma exactamente 100%', () => {
    const result = runSimulation(buildInput(xml))
    const sum = result.perActivity.reduce((acc, a) => acc + a.percentOfTotal, 0)
    expect(sum).toBeCloseTo(100, 1)
  })

  it('tiempo total = 5 × 30 min = 150 min', () => {
    const result = runSimulation(buildInput(xml, { executionTimeMinutes: 30 }))
    expect(result.totalTimeMinutes).toBeCloseTo(150)
  })
})

// ─── medium-with-gateways.bpmn ───────────────────────────────────────────────
describe('simulación / medium-with-gateways.bpmn', () => {
  const xml = loadSample('medium-with-gateways.bpmn')

  it('corre sin lanzar error con costos dummy', () => {
    expect(() => runSimulation(buildInput(xml))).not.toThrow()
  })

  it('no genera warnings (sin loops)', () => {
    const result = runSimulation(buildInput(xml))
    expect(result.warnings).toHaveLength(0)
  })

  it('perActivity tiene exactamente 11 entradas', () => {
    const result = runSimulation(buildInput(xml))
    expect(result.perActivity).toHaveLength(11)
  })

  it('actividades antes de cualquier gateway tienen prob = 1.0', () => {
    const result = runSimulation(buildInput(xml))
    const recv = result.perActivity.find((a) => a.bpmnElementId === 'task_recv')
    const valid = result.perActivity.find((a) => a.bpmnElementId === 'task_valid')
    expect(recv?.executionProbability).toBeCloseTo(1.0)
    expect(valid?.executionProbability).toBeCloseTo(1.0)
  })

  it('gateway XOR con probs 0.7/0.3 propaga correctamente', () => {
    // gw_datos: completo(idx 0)=0.7, incompleto(idx 1)=0.3
    const result = runSimulation(
      buildInput(xml, {
        gatewayProbsOverride: { gw_datos: [0.7, 0.3] },
      })
    )
    const score = result.perActivity.find((a) => a.bpmnElementId === 'task_score')
    const pedirDocs = result.perActivity.find((a) => a.bpmnElementId === 'task_pedir_docs')
    expect(score?.executionProbability).toBeCloseTo(0.7, 1)
    expect(pedirDocs?.executionProbability).toBeCloseTo(0.3, 1)
  })

  it('gateway paralelo AND: ambas ramas tienen prob igual a la entrada', () => {
    // gw_datos completo=1.0 para simplificar, gw_parallel_split AND ambas =1
    const result = runSimulation(
      buildInput(xml, {
        gatewayProbsOverride: {
          gw_datos: [1.0, 0.0],      // siempre el camino completo
          gw_decision: [0.5, 0.5],
        },
      })
    )
    const bureau = result.perActivity.find((a) => a.bpmnElementId === 'task_bureau')
    const empleador = result.perActivity.find((a) => a.bpmnElementId === 'task_empleador')
    // Ambas ramas del AND deben tener prob similar (≈ prob del score)
    expect(bureau?.executionProbability).toBeGreaterThan(0)
    expect(empleador?.executionProbability).toBeGreaterThan(0)
    expect(Math.abs((bureau?.executionProbability ?? 0) - (empleador?.executionProbability ?? 0))).toBeLessThan(0.01)
  })

  it('perActivity ordenado descendente por costo', () => {
    const result = runSimulation(buildInput(xml))
    for (let i = 0; i < result.perActivity.length - 1; i++) {
      expect(result.perActivity[i].expectedTotalCost).toBeGreaterThanOrEqual(
        result.perActivity[i + 1].expectedTotalCost
      )
    }
  })
})

// ─── complex-with-loop.bpmn ───────────────────────────────────────────────────
describe('simulación / complex-with-loop.bpmn', () => {
  const xml = loadSample('complex-with-loop.bpmn')

  it('corre sin lanzar error (a pesar del loop)', () => {
    expect(() => runSimulation(buildInput(xml))).not.toThrow()
  })

  it('🚨 genera al menos un warning de loop detectado', () => {
    const result = runSimulation(buildInput(xml))
    expect(result.warnings.length).toBeGreaterThan(0)
    const hasLoopWarning = result.warnings.some(
      (w) => w.toLowerCase().includes('loop') || w.toLowerCase().includes('ciclo')
    )
    expect(hasLoopWarning).toBe(true)
  })

  it('el warning menciona el nodo "Inspección visual y funcional"', () => {
    const result = runSimulation(buildInput(xml))
    const loopWarning = result.warnings.find(
      (w) => w.toLowerCase().includes('loop') || w.toLowerCase().includes('ciclo')
    )
    expect(loopWarning).toContain('Inspección visual y funcional')
  })

  it('resultado es válido a pesar del loop: costo > 0 y 9 actividades', () => {
    const result = runSimulation(buildInput(xml, { directCostFixed: 100 }))
    expect(result.totalCost).toBeGreaterThan(0)
    expect(result.perActivity).toHaveLength(9)
  })

  it('con probabilidades reales (60% aprobado, 30% correcciones, 10% rechazo)', () => {
    const result = runSimulation(
      buildInput(xml, {
        directCostFixed: 200,
        overheadPercentage: 0.15,
        gatewayProbsOverride: {
          // gw_resultado: 3 salidas — aprobado 0.6, obs_menores 0.3, rechazo 0.1
          gw_resultado: [0.6, 0.3, 0.1],
        },
      })
    )

    expect(result.totalCost).toBeGreaterThan(0)
    expect(result.warnings.length).toBeGreaterThan(0)

    // task_inspeccion tiene prob alta (es el nodo más costoso del proceso con loop)
    const inspeccion = result.perActivity.find((a) => a.bpmnElementId === 'task_inspeccion')
    expect(inspeccion).toBeDefined()
    expect(inspeccion!.executionProbability).toBeGreaterThan(0)

    // task_correcciones tiene prob = prob del camino de correcciones ≈ 0.3
    const correcciones = result.perActivity.find((a) => a.bpmnElementId === 'task_correcciones')
    expect(correcciones).toBeDefined()
    expect(correcciones!.executionProbability).toBeCloseTo(0.3, 1)

    // task_rechazo tiene prob ≈ 0.1
    const rechazo = result.perActivity.find((a) => a.bpmnElementId === 'task_rechazo')
    expect(rechazo).toBeDefined()
    expect(rechazo!.executionProbability).toBeCloseTo(0.1, 1)
  })

  it('percentOfTotal suma ≈ 100%', () => {
    const result = runSimulation(buildInput(xml))
    if (result.perActivity.length > 0) {
      const sum = result.perActivity.reduce((acc, a) => acc + a.percentOfTotal, 0)
      expect(sum).toBeCloseTo(100, 0)
    }
  })
})
