/**
 * Tests permanentes de persistencia en IndexedDB vía Dexie.
 * Usan fake-indexeddb (parcheado en tests/setup.ts) para correr en Node.
 *
 * Cada test limpia las tablas en beforeEach para garantizar aislamiento.
 * Verifican que los repositorios escriben y recuperan datos correctamente.
 */

import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { db } from '@/persistence/db'
import {
  processRepo,
  activityRepo,
  gatewayRepo,
  resourceRepo,
  simulationRepo,
} from '@/persistence/repositories'
import type { Process, Activity, GatewayConfig, Resource, Simulation, SimulationResult } from '@/domain/types'

// ─── Fixtures ─────────────────────────────────────────────────────────────────

function makeProcess(id = 'proc-1'): Process {
  return {
    id,
    name: 'Proceso de prueba',
    clientName: 'Cliente Test',
    bpmnXml: '<definitions/>',
    currency: 'USD',
    overheadPercentage: 0.2,
    annualFrequency: 1000,
    analysisHorizonYears: 3,
    automationInvestment: 0,
    createdAt: 1000,
    updatedAt: 2000,
    groupId: null,
    tags: [],
  }
}

function makeActivity(id: string, processId: string, bpmnElementId: string): Activity {
  return {
    id,
    processId,
    bpmnElementId,
    name: `Tarea ${bpmnElementId}`,
    type: 'task',
    directCostFixed: 100,
    executionTimeMinutes: 30,
    assignedResources: [{ resourceId: 'res-1', utilizationMinutes: 15, units: 1 }],
    automatable: false,
    automatedCostFixed: 0,
    automatedTimeMinutes: 0,
    automatedAssignedResources: [],
  }
}

function makeGateway(id: string, processId: string, bpmnElementId: string): GatewayConfig {
  return {
    id,
    processId,
    bpmnElementId,
    gatewayType: 'exclusive',
    branches: [
      { flowId: 'f1', targetElementId: 'taskA', probability: 0.7 },
      { flowId: 'f2', targetElementId: 'taskB', probability: 0.3 },
    ],
  }
}

function makeResource(id: string, processId: string): Resource {
  return {
    id,
    processId,
    name: 'Analista',
    type: 'role',
    costPerHour: 60,
  }
}

function makeSimulation(id: string, processId: string): Simulation {
  const result: SimulationResult = {
    totalCost: 1200,
    totalDirectCost: 1000,
    totalIndirectCost: 200,
    totalTimeMinutes: 90,
    perActivity: [],
    perResource: [],
    warnings: [],
  }
  return { id, processId, executedAt: 3000, result }
}

// ─── Setup / Teardown ─────────────────────────────────────────────────────────

beforeEach(async () => {
  await db.open()
  await db.transaction(
    'rw',
    [db.processes, db.activities, db.gateways, db.resources, db.simulations],
    async () => {
      await Promise.all([
        db.processes.clear(),
        db.activities.clear(),
        db.gateways.clear(),
        db.resources.clear(),
        db.simulations.clear(),
      ])
    }
  )
})

afterEach(async () => {
  await db.close()
})

// ─── Process ─────────────────────────────────────────────────────────────────
describe('IndexedDB / processRepo', () => {
  it('guarda un proceso y lo recupera por id', async () => {
    const proc = makeProcess()
    await processRepo.save(proc)
    const retrieved = await processRepo.getById('proc-1')
    expect(retrieved).toBeDefined()
    expect(retrieved!.name).toBe('Proceso de prueba')
    expect(retrieved!.currency).toBe('USD')
    expect(retrieved!.overheadPercentage).toBe(0.2)
  })

  it('getById devuelve undefined para id inexistente', async () => {
    const result = await processRepo.getById('no-existe')
    expect(result).toBeUndefined()
  })

  it('getAll devuelve todos los procesos guardados', async () => {
    await processRepo.save(makeProcess('p1'))
    await processRepo.save(makeProcess('p2'))
    await processRepo.save(makeProcess('p3'))
    const all = await processRepo.getAll()
    expect(all).toHaveLength(3)
  })

  it('save sobrescribe (put) un proceso existente', async () => {
    const proc = makeProcess()
    await processRepo.save(proc)
    await processRepo.save({ ...proc, name: 'Nombre actualizado', updatedAt: 9999 })
    const updated = await processRepo.getById('proc-1')
    expect(updated!.name).toBe('Nombre actualizado')
    expect(updated!.updatedAt).toBe(9999)
  })

  it('delete elimina el proceso y sus entidades relacionadas en cascada', async () => {
    const proc = makeProcess('proc-del')
    await processRepo.save(proc)
    await activityRepo.saveMany([makeActivity('a1', 'proc-del', 'task1')])
    await gatewayRepo.saveMany([makeGateway('g1', 'proc-del', 'gw1')])
    await resourceRepo.save(makeResource('r1', 'proc-del'))

    await processRepo.delete('proc-del')

    expect(await processRepo.getById('proc-del')).toBeUndefined()
    expect(await activityRepo.getByProcess('proc-del')).toHaveLength(0)
    expect(await gatewayRepo.getByProcess('proc-del')).toHaveLength(0)
    expect(await resourceRepo.getByProcess('proc-del')).toHaveLength(0)
  })
})

// ─── Activity ─────────────────────────────────────────────────────────────────
describe('IndexedDB / activityRepo', () => {
  const processId = 'proc-act'

  it('saveMany y getByProcess devuelven las mismas actividades', async () => {
    const acts = [
      makeActivity('a1', processId, 'task1'),
      makeActivity('a2', processId, 'task2'),
      makeActivity('a3', processId, 'task3'),
    ]
    await activityRepo.saveMany(acts)
    const retrieved = await activityRepo.getByProcess(processId)
    expect(retrieved).toHaveLength(3)
    const ids = retrieved.map((a) => a.id).sort()
    expect(ids).toEqual(['a1', 'a2', 'a3'])
  })

  it('persiste assignedResources (array anidado) correctamente', async () => {
    const act = makeActivity('a1', processId, 'task1')
    await activityRepo.save(act)
    const [retrieved] = await activityRepo.getByProcess(processId)
    expect(retrieved.assignedResources).toHaveLength(1)
    expect(retrieved.assignedResources[0].resourceId).toBe('res-1')
    expect(retrieved.assignedResources[0].utilizationMinutes).toBe(15)
    expect(retrieved.assignedResources[0].units).toBe(1)
  })

  it('no devuelve actividades de otro proceso', async () => {
    await activityRepo.save(makeActivity('a-other', 'otro-proc', 'task1'))
    const result = await activityRepo.getByProcess(processId)
    expect(result).toHaveLength(0)
  })
})

// ─── GatewayConfig ────────────────────────────────────────────────────────────
describe('IndexedDB / gatewayRepo', () => {
  const processId = 'proc-gw'

  it('saveMany y getByProcess devuelven la config con branches correctas', async () => {
    const gw = makeGateway('g1', processId, 'gw_decision')
    await gatewayRepo.saveMany([gw])
    const [retrieved] = await gatewayRepo.getByProcess(processId)
    expect(retrieved.bpmnElementId).toBe('gw_decision')
    expect(retrieved.gatewayType).toBe('exclusive')
    expect(retrieved.branches).toHaveLength(2)
    expect(retrieved.branches[0].probability).toBe(0.7)
    expect(retrieved.branches[1].probability).toBe(0.3)
  })

  it('save individual actualiza la config existente', async () => {
    const gw = makeGateway('g1', processId, 'gw1')
    await gatewayRepo.save(gw)
    const updated: GatewayConfig = {
      ...gw,
      branches: [
        { flowId: 'f1', targetElementId: 'taskA', probability: 0.6 },
        { flowId: 'f2', targetElementId: 'taskB', probability: 0.4 },
      ],
    }
    await gatewayRepo.save(updated)
    const [retrieved] = await gatewayRepo.getByProcess(processId)
    expect(retrieved.branches[0].probability).toBe(0.6)
  })
})

// ─── Resource ─────────────────────────────────────────────────────────────────
describe('IndexedDB / resourceRepo', () => {
  const processId = 'proc-res'

  it('save y getByProcess recupera el recurso correctamente', async () => {
    await resourceRepo.save(makeResource('r1', processId))
    const [retrieved] = await resourceRepo.getByProcess(processId)
    expect(retrieved.name).toBe('Analista')
    expect(retrieved.costPerHour).toBe(60)
    expect(retrieved.type).toBe('role')
  })

  it('delete elimina un recurso por id', async () => {
    await resourceRepo.save(makeResource('r1', processId))
    await resourceRepo.save(makeResource('r2', processId))
    await resourceRepo.delete('r1')
    const remaining = await resourceRepo.getByProcess(processId)
    expect(remaining).toHaveLength(1)
    expect(remaining[0].id).toBe('r2')
  })

  it('múltiples recursos para el mismo proceso', async () => {
    const types = ['role', 'system', 'equipment'] as const
    for (const [i, type] of types.entries()) {
      await resourceRepo.save({ ...makeResource(`r${i}`, processId), type })
    }
    const all = await resourceRepo.getByProcess(processId)
    expect(all).toHaveLength(3)
    const typeSet = new Set(all.map((r) => r.type))
    expect(typeSet.has('role')).toBe(true)
    expect(typeSet.has('system')).toBe(true)
    expect(typeSet.has('equipment')).toBe(true)
  })
})

// ─── Simulation ───────────────────────────────────────────────────────────────
describe('IndexedDB / simulationRepo', () => {
  const processId = 'proc-sim'

  it('save y getLatestByProcess recuperan el snapshot de resultado', async () => {
    const sim = makeSimulation('sim-1', processId)
    await simulationRepo.save(sim)
    const retrieved = await simulationRepo.getLatestByProcess(processId)
    expect(retrieved).toBeDefined()
    expect(retrieved!.result.totalCost).toBe(1200)
    expect(retrieved!.result.totalDirectCost).toBe(1000)
    expect(retrieved!.result.totalIndirectCost).toBe(200)
  })

  it('getLatestByProcess devuelve undefined si no hay simulaciones', async () => {
    const result = await simulationRepo.getLatestByProcess('proc-sin-sim')
    expect(result).toBeUndefined()
  })

  it('múltiples simulaciones: getByProcess las recupera todas', async () => {
    await simulationRepo.save({ ...makeSimulation('s1', processId), executedAt: 1000 })
    await simulationRepo.save({ ...makeSimulation('s2', processId), executedAt: 2000 })
    await simulationRepo.save({ ...makeSimulation('s3', processId), executedAt: 3000 })
    const all = await simulationRepo.getByProcess(processId)
    expect(all).toHaveLength(3)
  })

  it('persiste warnings en el resultado (campo libre)', async () => {
    const sim: Simulation = {
      ...makeSimulation('s-warn', processId),
      result: {
        ...makeSimulation('s-warn', processId).result,
        warnings: ['Loop detectado en taskX'],
      },
    }
    await simulationRepo.save(sim)
    const retrieved = await simulationRepo.getLatestByProcess(processId)
    expect(retrieved!.result.warnings).toHaveLength(1)
    expect(retrieved!.result.warnings[0]).toContain('Loop detectado')
  })
})

// ─── Round-trip completo (simula el flujo de ImportPage) ─────────────────────
describe('IndexedDB / round-trip proceso completo', () => {
  it('guarda proceso + actividades + gateways + recursos, luego los recupera íntegros', async () => {
    const proc = makeProcess('rt-proc')
    const acts = [
      makeActivity('rt-a1', 'rt-proc', 'taskA'),
      makeActivity('rt-a2', 'rt-proc', 'taskB'),
    ]
    const gws = [makeGateway('rt-g1', 'rt-proc', 'gw1')]
    const resources = [makeResource('rt-r1', 'rt-proc')]

    await Promise.all([
      processRepo.save(proc),
      activityRepo.saveMany(acts),
      gatewayRepo.saveMany(gws),
      resourceRepo.save(resources[0]),
    ])

    // Simular recarga de página
    const [retrievedProc, retrievedActs, retrievedGws, retrievedRes] = await Promise.all([
      processRepo.getById('rt-proc'),
      activityRepo.getByProcess('rt-proc'),
      gatewayRepo.getByProcess('rt-proc'),
      resourceRepo.getByProcess('rt-proc'),
    ])

    expect(retrievedProc!.id).toBe('rt-proc')
    expect(retrievedActs).toHaveLength(2)
    expect(retrievedGws).toHaveLength(1)
    expect(retrievedGws[0].branches).toHaveLength(2)
    expect(retrievedRes).toHaveLength(1)
    expect(retrievedRes[0].costPerHour).toBe(60)
  })
})
