import { describe, it, expect } from 'vitest'
import { parseBpmnXml, extractActivityElements, extractGatewayElements } from '@/domain/bpmn-parser'
import { runSimulation } from '@/domain/simulation'
import type { Activity, Resource, GatewayConfig, SimulationInput } from '@/domain/types'

// BPMN mínimo para tests — proceso lineal de 2 tareas
const LINEAR_BPMN = `<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" targetNamespace="test">
  <process id="proc1" isExecutable="false">
    <startEvent id="start"><outgoing>f1</outgoing></startEvent>
    <task id="taskA" name="Tarea A"><incoming>f1</incoming><outgoing>f2</outgoing></task>
    <task id="taskB" name="Tarea B"><incoming>f2</incoming><outgoing>f3</outgoing></task>
    <endEvent id="end"><incoming>f3</incoming></endEvent>
    <sequenceFlow id="f1" sourceRef="start" targetRef="taskA"/>
    <sequenceFlow id="f2" sourceRef="taskA" targetRef="taskB"/>
    <sequenceFlow id="f3" sourceRef="taskB" targetRef="end"/>
  </process>
</definitions>`

// BPMN con gateway XOR
const XOR_BPMN = `<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" targetNamespace="test">
  <process id="proc2" isExecutable="false">
    <startEvent id="start"><outgoing>f1</outgoing></startEvent>
    <task id="taskA" name="Evaluar"><incoming>f1</incoming><outgoing>f2</outgoing></task>
    <exclusiveGateway id="gw1" name="Decision"><incoming>f2</incoming><outgoing>f3</outgoing><outgoing>f4</outgoing></exclusiveGateway>
    <task id="taskB" name="Camino Si"><incoming>f3</incoming><outgoing>f5</outgoing></task>
    <task id="taskC" name="Camino No"><incoming>f4</incoming><outgoing>f6</outgoing></task>
    <endEvent id="end"><incoming>f5</incoming><incoming>f6</incoming></endEvent>
    <sequenceFlow id="f1" sourceRef="start" targetRef="taskA"/>
    <sequenceFlow id="f2" sourceRef="taskA" targetRef="gw1"/>
    <sequenceFlow id="f3" sourceRef="gw1" targetRef="taskB"/>
    <sequenceFlow id="f4" sourceRef="gw1" targetRef="taskC"/>
    <sequenceFlow id="f5" sourceRef="taskB" targetRef="end"/>
    <sequenceFlow id="f6" sourceRef="taskC" targetRef="end"/>
  </process>
</definitions>`

describe('parseBpmnXml', () => {
  it('parsea proceso lineal y extrae nodos y flujos', () => {
    const graph = parseBpmnXml(LINEAR_BPMN)
    expect(graph.nodes.size).toBe(4) // start, taskA, taskB, end
    expect(graph.flows.size).toBe(3)
    expect(graph.startEventIds).toEqual(['start'])
  })

  it('extrae actividades correctamente', () => {
    const graph = parseBpmnXml(LINEAR_BPMN)
    const activities = extractActivityElements(graph)
    expect(activities).toHaveLength(2)
    expect(activities[0].bpmnElementId).toBe('taskA')
    expect(activities[1].bpmnElementId).toBe('taskB')
  })

  it('extrae gateways divergentes', () => {
    const graph = parseBpmnXml(XOR_BPMN)
    const gateways = extractGatewayElements(graph)
    expect(gateways).toHaveLength(1)
    expect(gateways[0].bpmnElementId).toBe('gw1')
    expect(gateways[0].outgoing).toHaveLength(2)
  })

  it('lanza error si el XML no tiene proceso', () => {
    expect(() => parseBpmnXml('<bad/>')).toThrow()
  })
})

describe('runSimulation', () => {
  const makeResource = (id: string, name: string, costPerHour: number): Resource => ({
    id,
    processId: 'proc1',
    name,
    type: 'role',
    costPerHour,
  })

  const makeActivity = (
    bpmnElementId: string,
    name: string,
    directCost: number,
    minutes: number,
    resources: Array<{ resourceId: string; utilizationMinutes: number; units?: number }> = []
  ): Activity => ({
    id: `act-${bpmnElementId}`,
    processId: 'proc1',
    bpmnElementId,
    name,
    type: 'task',
    directCostFixed: directCost,
    executionTimeMinutes: minutes,
    assignedResources: resources.map((r) => ({ ...r, units: r.units ?? 1 })),
    automatable: false,
    automatedCostFixed: 0,
    automatedTimeMinutes: 0,
    automatedAssignedResources: [],
  })

  it('calcula costo total sin recursos — proceso lineal', () => {
    const activityA = makeActivity('taskA', 'Tarea A', 100, 30)
    const activityB = makeActivity('taskB', 'Tarea B', 200, 60)

    const input: SimulationInput = {
      processXml: LINEAR_BPMN,
      activities: new Map([
        ['taskA', activityA],
        ['taskB', activityB],
      ]),
      gateways: new Map(),
      resources: new Map(),
      globalSettings: { currency: 'USD', overheadPercentage: 0 },
    }

    const result = runSimulation(input)
    expect(result.totalDirectCost).toBe(300)
    expect(result.totalIndirectCost).toBe(0)
    expect(result.totalCost).toBe(300)
    expect(result.warnings).toHaveLength(0)
  })

  it('aplica overhead correctamente', () => {
    const activityA = makeActivity('taskA', 'Tarea A', 100, 30)

    const input: SimulationInput = {
      processXml: LINEAR_BPMN,
      activities: new Map([['taskA', activityA]]),
      gateways: new Map(),
      resources: new Map(),
      globalSettings: { currency: 'USD', overheadPercentage: 0.2 },
    }

    const result = runSimulation(input)
    expect(result.totalDirectCost).toBe(100)
    expect(result.totalIndirectCost).toBeCloseTo(20)
    expect(result.totalCost).toBeCloseTo(120)
  })

  it('incluye costo de recursos asignados', () => {
    const resource = makeResource('res1', 'Analista', 60) // $60/hora
    const activityA = makeActivity('taskA', 'Tarea A', 0, 60, [
      { resourceId: 'res1', utilizationMinutes: 60, units: 1 }, // 1 hora completa
    ])

    const input: SimulationInput = {
      processXml: LINEAR_BPMN,
      activities: new Map([['taskA', activityA]]),
      gateways: new Map(),
      resources: new Map([['res1', resource]]),
      globalSettings: { currency: 'USD', overheadPercentage: 0 },
    }

    const result = runSimulation(input)
    expect(result.totalDirectCost).toBeCloseTo(60)
  })

  it('propaga probabilidad 0.7/0.3 en gateway XOR', () => {
    const actA = makeActivity('taskA', 'Evaluar', 100, 30)
    const actB = makeActivity('taskB', 'Camino Si', 200, 60)
    const actC = makeActivity('taskC', 'Camino No', 100, 30)

    const gw: GatewayConfig = {
      id: 'gw-1',
      processId: 'proc2',
      bpmnElementId: 'gw1',
      gatewayType: 'exclusive',
      branches: [
        { flowId: 'f3', targetElementId: 'taskB', probability: 0.7 },
        { flowId: 'f4', targetElementId: 'taskC', probability: 0.3 },
      ],
    }

    const input: SimulationInput = {
      processXml: XOR_BPMN,
      activities: new Map([
        ['taskA', actA],
        ['taskB', actB],
        ['taskC', actC],
      ]),
      gateways: new Map([['gw1', gw]]),
      resources: new Map(),
      globalSettings: { currency: 'USD', overheadPercentage: 0 },
    }

    const result = runSimulation(input)
    const bResult = result.perActivity.find((a) => a.bpmnElementId === 'taskB')
    const cResult = result.perActivity.find((a) => a.bpmnElementId === 'taskC')

    expect(bResult?.executionProbability).toBeCloseTo(0.7, 1)
    expect(cResult?.executionProbability).toBeCloseTo(0.3, 1)
    // Costo esperado de B = 200 * 0.7 = 140
    expect(bResult?.expectedDirectCost).toBeCloseTo(140)
    // Costo esperado de C = 100 * 0.3 = 30
    expect(cResult?.expectedDirectCost).toBeCloseTo(30)
  })

  it('ordena perActivity por costo total descendente', () => {
    const actA = makeActivity('taskA', 'Tarea A', 50, 10)
    const actB = makeActivity('taskB', 'Tarea B', 500, 60)

    const input: SimulationInput = {
      processXml: LINEAR_BPMN,
      activities: new Map([
        ['taskA', actA],
        ['taskB', actB],
      ]),
      gateways: new Map(),
      resources: new Map(),
      globalSettings: { currency: 'USD', overheadPercentage: 0 },
    }

    const result = runSimulation(input)
    expect(result.perActivity[0].bpmnElementId).toBe('taskB')
    expect(result.perActivity[1].bpmnElementId).toBe('taskA')
  })
})

// ─── Sprint 1: escenario 'automated' + actividades automatizables ─────────────

describe('runSimulation — scenario automated', () => {
  const makeAutoActivity = (
    bpmnElementId: string,
    opts: {
      directCost?: number; directTime?: number
      automatable?: boolean; automatedCost?: number; automatedTime?: number
      automatedAssignedResources?: import('@/domain/types').ActivityResourceAssignment[]
    } = {}
  ): Activity => ({
    id: `act-${bpmnElementId}`,
    processId: 'proc1',
    bpmnElementId,
    name: bpmnElementId,
    type: 'task',
    directCostFixed: opts.directCost ?? 100,
    executionTimeMinutes: opts.directTime ?? 60,
    assignedResources: [],
    automatable: opts.automatable ?? false,
    automatedCostFixed: opts.automatedCost ?? 0,
    automatedTimeMinutes: opts.automatedTime ?? 0,
    automatedAssignedResources: opts.automatedAssignedResources ?? [],
  })

  // ─── scenario=automated con costos en cero ────────────────────────────────

  it('scenario=automated con automatedCostFixed=0 y automatedTimeMinutes=0 devuelve cost=0 time=0 sin NaN', () => {
    const act = makeAutoActivity('taskA', {
      directCost: 500, directTime: 60,
      automatable: true, automatedCost: 0, automatedTime: 0,
    })

    const result = runSimulation({
      processXml: LINEAR_BPMN,
      activities: new Map([['taskA', act], ['taskB', makeAutoActivity('taskB')]]),
      gateways: new Map(),
      resources: new Map(),
      globalSettings: { currency: 'USD', overheadPercentage: 0 },
      scenario: 'automated',
    })

    const actResult = result.perActivity.find((a) => a.bpmnElementId === 'taskA')!
    expect(actResult).toBeDefined()
    expect(actResult.expectedDirectCost).toBe(0)
    expect(actResult.executionTimeMinutes).toBe(0)

    // Verificación explícita de ausencia de NaN en todos los campos numéricos
    expect(Number.isNaN(actResult.expectedDirectCost)).toBe(false)
    expect(Number.isNaN(actResult.expectedIndirectCost)).toBe(false)
    expect(Number.isNaN(actResult.expectedTotalCost)).toBe(false)
    expect(Number.isNaN(actResult.percentOfTotal)).toBe(false)
    expect(Number.isNaN(result.totalCost)).toBe(false)
  })

  it('actividad NO automatable mantiene costos originales en scenario=automated', () => {
    const act = makeAutoActivity('taskA', {
      directCost: 200, directTime: 30,
      automatable: false, automatedCost: 0, automatedTime: 0,
    })

    const result = runSimulation({
      processXml: LINEAR_BPMN,
      activities: new Map([['taskA', act], ['taskB', makeAutoActivity('taskB')]]),
      gateways: new Map(),
      resources: new Map(),
      globalSettings: { currency: 'USD', overheadPercentage: 0 },
      scenario: 'automated',
    })

    const actResult = result.perActivity.find((a) => a.bpmnElementId === 'taskA')!
    // automatable=false → usa costo original
    expect(actResult.expectedDirectCost).toBeCloseTo(200)
    expect(actResult.executionTimeMinutes).toBe(30)
  })

  // ─── La probabilidad de ejecución NO depende del scenario ────────────────

  it('probabilidad de ejecución NO cambia entre scenario=actual y scenario=automated', () => {
    // Usamos el BPMN XOR: taskB tiene prob 0.7, taskC tiene prob 0.3
    const XOR_BPMN_LOCAL = `<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" targetNamespace="test">
  <process id="p" isExecutable="false">
    <startEvent id="start"><outgoing>f1</outgoing></startEvent>
    <task id="taskA" name="A"><incoming>f1</incoming><outgoing>f2</outgoing></task>
    <exclusiveGateway id="gw1"><incoming>f2</incoming><outgoing>f3</outgoing><outgoing>f4</outgoing></exclusiveGateway>
    <task id="taskB" name="B"><incoming>f3</incoming><outgoing>f5</outgoing></task>
    <task id="taskC" name="C"><incoming>f4</incoming><outgoing>f6</outgoing></task>
    <endEvent id="end"><incoming>f5</incoming><incoming>f6</incoming></endEvent>
    <sequenceFlow id="f1" sourceRef="start" targetRef="taskA"/>
    <sequenceFlow id="f2" sourceRef="taskA" targetRef="gw1"/>
    <sequenceFlow id="f3" sourceRef="gw1" targetRef="taskB"/>
    <sequenceFlow id="f4" sourceRef="gw1" targetRef="taskC"/>
    <sequenceFlow id="f5" sourceRef="taskB" targetRef="end"/>
    <sequenceFlow id="f6" sourceRef="taskC" targetRef="end"/>
  </process>
</definitions>`

    const activities = new Map([
      ['taskA', makeAutoActivity('taskA', { automatable: true, automatedCost: 5, automatedTime: 2 })],
      ['taskB', makeAutoActivity('taskB', { automatable: true, automatedCost: 0, automatedTime: 0 })],
      ['taskC', makeAutoActivity('taskC', { automatable: false })],
    ])

    const gateways = new Map([['gw1', {
      id: 'gw1', processId: 'proc1', bpmnElementId: 'gw1', gatewayType: 'exclusive' as const,
      branches: [
        { flowId: 'f3', targetElementId: 'taskB', probability: 0.7 },
        { flowId: 'f4', targetElementId: 'taskC', probability: 0.3 },
      ],
    }]])

    const baseInput = {
      processXml: XOR_BPMN_LOCAL,
      activities,
      gateways,
      resources: new Map(),
      globalSettings: { currency: 'USD', overheadPercentage: 0 },
    }

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

    // taskA tiene prob 1.0 en ambos
    const aActual = resultActual.perActivity.find((a) => a.bpmnElementId === 'taskA')!
    const aAuto = resultAutomated.perActivity.find((a) => a.bpmnElementId === 'taskA')!
    expect(aActual.executionProbability).toBeCloseTo(1.0)
    expect(aAuto.executionProbability).toBeCloseTo(1.0)

    // taskB tiene prob 0.7 en ambos (el scenario no cambia el grafo)
    const bActual = resultActual.perActivity.find((a) => a.bpmnElementId === 'taskB')!
    const bAuto = resultAutomated.perActivity.find((a) => a.bpmnElementId === 'taskB')!
    expect(bActual.executionProbability).toBeCloseTo(0.7)
    expect(bAuto.executionProbability).toBeCloseTo(0.7)

    // taskC tiene prob 0.3 en ambos
    const cActual = resultActual.perActivity.find((a) => a.bpmnElementId === 'taskC')!
    const cAuto = resultAutomated.perActivity.find((a) => a.bpmnElementId === 'taskC')!
    expect(cActual.executionProbability).toBeCloseTo(0.3)
    expect(cAuto.executionProbability).toBeCloseTo(0.3)

    // Los costos SÍ cambian para actividades automatable=true
    // taskB en actual: directCostFixed=100; en automated: automatedCostFixed=0
    expect(bActual.expectedDirectCost).toBeGreaterThan(0)
    expect(bAuto.expectedDirectCost).toBe(0)

    // taskC no es automatable → costos iguales en ambos
    expect(cActual.expectedDirectCost).toBeCloseTo(cAuto.expectedDirectCost)
  })

  it('NaN no aparece en ningún campo cuando TODOS los costos son cero', () => {
    const activities = new Map([
      ['taskA', makeAutoActivity('taskA', { directCost: 0, directTime: 0, automatable: true, automatedCost: 0, automatedTime: 0 })],
      ['taskB', makeAutoActivity('taskB', { directCost: 0, directTime: 0, automatable: true, automatedCost: 0, automatedTime: 0 })],
    ])

    for (const scenario of ['actual', 'automated'] as const) {
      const result = runSimulation({
        processXml: LINEAR_BPMN,
        activities,
        gateways: new Map(),
        resources: new Map(),
        globalSettings: { currency: 'USD', overheadPercentage: 0.2 },
        scenario,
      })

      expect(Number.isNaN(result.totalCost)).toBe(false)
      expect(Number.isNaN(result.totalDirectCost)).toBe(false)
      expect(Number.isNaN(result.totalIndirectCost)).toBe(false)

      for (const act of result.perActivity) {
        expect(Number.isNaN(act.expectedDirectCost)).toBe(false)
        expect(Number.isNaN(act.expectedIndirectCost)).toBe(false)
        expect(Number.isNaN(act.expectedTotalCost)).toBe(false)
        expect(Number.isNaN(act.percentOfTotal)).toBe(false)
        expect(Number.isNaN(act.executionProbability)).toBe(false)
      }
    }
  })
})

// ─── Sprint 2: modelo utilizationMinutes + units ──────────────────────────────

describe('runSimulation — recursos con modelo utilizationMinutes + units', () => {
  const makeRes = (id: string, costPerHour: number): Resource => ({
    id, processId: 'proc1', name: `Recurso ${id}`, type: 'role', costPerHour,
  })

  const makeAct = (
    bpmnElementId: string,
    directCost: number,
    minutes: number,
    resources: import('@/domain/types').ActivityResourceAssignment[] = []
  ): Activity => ({
    id: `act-${bpmnElementId}`, processId: 'proc1', bpmnElementId,
    name: bpmnElementId, type: 'task',
    directCostFixed: directCost, executionTimeMinutes: minutes,
    assignedResources: resources,
    automatable: false, automatedCostFixed: 0, automatedTimeMinutes: 0,
    automatedAssignedResources: [],
  })

  it('actividad sin recursos: costo = directCostFixed', () => {
    const act = makeAct('t1', 200, 30)
    const result = runSimulation({
      processXml: LINEAR_BPMN,
      activities: new Map([['taskA', { ...act, bpmnElementId: 'taskA' }]]),
      gateways: new Map(), resources: new Map(),
      globalSettings: { currency: 'USD', overheadPercentage: 0 },
    })
    expect(result.totalDirectCost).toBe(200)
  })

  it('actividad con 1 recurso: costo = fijo + costPerHour × utilizationMinutes/60 × units × execProb', () => {
    // Analista: $120/h, 30 min, 1 unidad → $60. Fijo: $50. Total: $110
    const act = makeAct('taskA', 50, 60, [{ resourceId: 'r1', utilizationMinutes: 30, units: 1 }])
    const result = runSimulation({
      processXml: LINEAR_BPMN,
      activities: new Map([['taskA', act]]),
      gateways: new Map(),
      resources: new Map([['r1', makeRes('r1', 120)]]),
      globalSettings: { currency: 'USD', overheadPercentage: 0 },
    })
    const actResult = result.perActivity.find((a) => a.bpmnElementId === 'taskA')
    expect(actResult?.expectedDirectCost).toBeCloseTo(110)
  })

  it('actividad con 2 recursos: costo = fijo + suma de costos de recursos', () => {
    // r1: $60/h × 30min/60 × 1 = $30. r2: $40/h × 60min/60 × 1 = $40. Fijo: $20. Total: $90
    const act = makeAct('taskA', 20, 60, [
      { resourceId: 'r1', utilizationMinutes: 30, units: 1 },
      { resourceId: 'r2', utilizationMinutes: 60, units: 1 },
    ])
    const result = runSimulation({
      processXml: LINEAR_BPMN,
      activities: new Map([['taskA', act]]),
      gateways: new Map(),
      resources: new Map([['r1', makeRes('r1', 60)], ['r2', makeRes('r2', 40)]]),
      globalSettings: { currency: 'USD', overheadPercentage: 0 },
    })
    const actResult = result.perActivity.find((a) => a.bpmnElementId === 'taskA')
    expect(actResult?.expectedDirectCost).toBeCloseTo(90)
  })

  it('units = 2: costo de recurso se duplica', () => {
    // $60/h × 30min/60 × 2 unidades = $60. Fijo: $0. Total: $60
    const act = makeAct('taskA', 0, 30, [{ resourceId: 'r1', utilizationMinutes: 30, units: 2 }])
    const result = runSimulation({
      processXml: LINEAR_BPMN,
      activities: new Map([['taskA', act]]),
      gateways: new Map(),
      resources: new Map([['r1', makeRes('r1', 60)]]),
      globalSettings: { currency: 'USD', overheadPercentage: 0 },
    })
    expect(result.totalDirectCost).toBeCloseTo(60)
  })

  it('utilizationMinutes = 0: el recurso no agrega costo', () => {
    const act = makeAct('taskA', 100, 60, [{ resourceId: 'r1', utilizationMinutes: 0, units: 1 }])
    const result = runSimulation({
      processXml: LINEAR_BPMN,
      activities: new Map([['taskA', act]]),
      gateways: new Map(),
      resources: new Map([['r1', makeRes('r1', 200)]]),
      globalSettings: { currency: 'USD', overheadPercentage: 0 },
    })
    expect(result.totalDirectCost).toBe(100)
  })

  it('resource no encontrado en el mapa: se ignora sin lanzar error', () => {
    const act = makeAct('taskA', 100, 60, [{ resourceId: 'no-existe', utilizationMinutes: 30, units: 1 }])
    expect(() => runSimulation({
      processXml: LINEAR_BPMN,
      activities: new Map([['taskA', act]]),
      gateways: new Map(), resources: new Map(),
      globalSettings: { currency: 'USD', overheadPercentage: 0 },
    })).not.toThrow()
    const result = runSimulation({
      processXml: LINEAR_BPMN,
      activities: new Map([['taskA', act]]),
      gateways: new Map(), resources: new Map(),
      globalSettings: { currency: 'USD', overheadPercentage: 0 },
    })
    expect(result.totalDirectCost).toBe(100)
  })
})
