import { describe, it, expect } from 'vitest'
import { mapSimVsmNode } from '../node-mapper'
import { parseSimVsmValue } from '../parser'
import { buildSyntheticSimVsmFile } from './fixtures'
import type { SimVsmParsedNode } from '../types'

let counter = 0
const idFactory = () => `n${++counter}`

function nodesOf(): SimVsmParsedNode[] {
  const result = parseSimVsmValue(buildSyntheticSimVsmFile(), 'x.json')
  if (!result.ok) throw new Error('fixture must parse')
  return result.alternatives[0].nodes
}

describe('mapSimVsmNode', () => {
  it('maps singleProcess with cycle time (first ProductTable row), availability, workers, VA-flag, MTTR in minutes, and comment as notes', () => {
    const node = nodesOf().find((n) => n.key === '2')!
    const outcome = mapSimVsmNode(node, idFactory)
    expect(outcome.supported).toBe(true)
    expect(outcome.vsmNode).toMatchObject({
      type: 'process',
      name: 'Stanzen',
      cycleTimeSec: 45,
      availabilityPct: 95,
      numWorkers: 2,
      isValueAdded: true,
      mttrMin: 10, // 600s / 60
      notes: 'Testkommentar',
      scrapRate: 2,
      provenance: 'imported',
    })
  })

  it('maps supplier to type "supplier"', () => {
    const node = nodesOf().find((n) => n.key === '1')!
    const outcome = mapSimVsmNode(node, idFactory)
    expect(outcome.vsmNode?.type).toBe('supplier')
    expect(outcome.vsmNode?.name).toBe('Lieferant Nord')
  })

  it('maps fifoStore to inventory with inventoryKind "fifo" and Capacity → inventoryMaxQuantity', () => {
    const node = nodesOf().find((n) => n.key === '3')!
    const outcome = mapSimVsmNode(node, idFactory)
    expect(outcome.vsmNode).toMatchObject({ type: 'inventory', inventoryKind: 'fifo', inventoryMaxQuantity: 20 })
  })

  it('customer row with Quantity but no Interval: demand stays UNSET (never fabricated) and is flagged demandIntervalMissing', () => {
    // Fixture's key=4 customer row is `{ Quantity: 100, Product: 'prod-a' }` —
    // no Interval field, the real-corpus-representative "Interval fehlt" case.
    const node = nodesOf().find((n) => n.key === '4')!
    const outcome = mapSimVsmNode(node, idFactory)
    expect(outcome.vsmNode?.type).toBe('customer')
    expect(outcome.vsmNode?.demand).toBeUndefined()
    expect(outcome.vsmNode?.quantity).toBeUndefined()
    expect(outcome.demandIntervalMissing).toBe(true)
  })

  it('quantity is NEVER filled from the customer order row, even with a valid Interval (Bestellmenge ≠ Bestand)', () => {
    const node: SimVsmParsedNode = {
      key: '4',
      category: 'item',
      simvsmClass: 'customer',
      nodeName: 'Kunde',
      params: new Map([['Produkte', { class: 'Produkte', type: 'tableGrid', value: { rows: [{ Quantity: 100, Interval: { durationInSec: 86400 } }] } }]]),
    }
    const outcome = mapSimVsmNode(node, idFactory)
    expect(outcome.vsmNode?.demand).toBe(100)
    expect(outcome.vsmNode?.quantity).toBeUndefined()
  })

  it('Interval normalization: 3600s (hourly) scales Quantity ×24 into Stk/Tag', () => {
    const node: SimVsmParsedNode = {
      key: '4',
      category: 'item',
      simvsmClass: 'customer',
      nodeName: 'Kunde',
      params: new Map([['Produkte', { class: 'Produkte', type: 'tableGrid', value: { rows: [{ Quantity: 50, Interval: { durationInSec: 3600 } }] } }]]),
    }
    const outcome = mapSimVsmNode(node, idFactory)
    expect(outcome.vsmNode?.demand).toBe(1200) // 50 × 86400/3600
    expect(outcome.demandIntervalMissing).toBeUndefined()
  })

  it('Interval normalization: 86400s (1 Kalendertag) leaves Quantity unchanged (×1)', () => {
    const node: SimVsmParsedNode = {
      key: '4',
      category: 'item',
      simvsmClass: 'customer',
      nodeName: 'Kunde',
      params: new Map([['Produkte', { class: 'Produkte', type: 'tableGrid', value: { rows: [{ Quantity: 200, Interval: { durationInSec: 86400 } }] } }]]),
    }
    const outcome = mapSimVsmNode(node, idFactory)
    expect(outcome.vsmNode?.demand).toBe(200)
  })

  it('Interval normalization: Interval 0 is treated as invalid — demand stays undefined, flagged demandIntervalMissing', () => {
    const node: SimVsmParsedNode = {
      key: '4',
      category: 'item',
      simvsmClass: 'customer',
      nodeName: 'Kunde',
      params: new Map([['Produkte', { class: 'Produkte', type: 'tableGrid', value: { rows: [{ Quantity: 50, Interval: { durationInSec: 0 } }] } }]]),
    }
    const outcome = mapSimVsmNode(node, idFactory)
    expect(outcome.vsmNode?.demand).toBeUndefined()
    expect(outcome.demandIntervalMissing).toBe(true)
  })

  it('cycleTimeSec/scrapRate from a ProductTable row are gated to process nodes — a customer node never gets a "Zykluszeit"', () => {
    const node: SimVsmParsedNode = {
      key: '4',
      category: 'item',
      simvsmClass: 'customer',
      nodeName: 'Kunde',
      params: new Map([
        ['Produkte', { class: 'Produkte', type: 'tableGrid', value: { rows: [{ Quantity: 10, Interval: { durationInSec: 86400 }, PlannedCycleTime: { durationInSec: 60 }, ScrapRate: 3 }] } }],
      ]),
    }
    const outcome = mapSimVsmNode(node, idFactory)
    expect(outcome.vsmNode?.cycleTimeSec).toBeUndefined()
    expect(outcome.vsmNode?.scrapRate).toBeUndefined()
  })

  it('maps productionControl to process with isValueAdded forced false (no dedicated PPS type)', () => {
    const node = nodesOf().find((n) => n.key === '5')!
    const outcome = mapSimVsmNode(node, idFactory)
    expect(outcome.supported).toBe(true)
    expect(outcome.vsmNode).toMatchObject({ type: 'process', isValueAdded: false })
    expect(outcome.confidence).toBeLessThan(1)
    expect(outcome.generalizedNote).toBeTruthy()
  })

  // P8.2a (Baustein 2 "PPS als echtes Datenfeld", KAR-878/KAR-986)
  it('maps productionControl with isPps: true (mapping-registry.ts targetIsPps)', () => {
    const node = nodesOf().find((n) => n.key === '5')!
    const outcome = mapSimVsmNode(node, idFactory)
    expect(outcome.vsmNode?.isPps).toBe(true)
  })

  it('reports noteVSM as unsupported with a reason, never a fabricated node', () => {
    const node = nodesOf().find((n) => n.key === '6')!
    const outcome = mapSimVsmNode(node, idFactory)
    expect(outcome.supported).toBe(false)
    expect(outcome.vsmNode).toBeNull()
    expect(outcome.reasonDe).toBeTruthy()
  })

  it('reports foreignOrders as unsupported (Capability-Matrix REJECT)', () => {
    const node = nodesOf().find((n) => n.key === '7')!
    const outcome = mapSimVsmNode(node, idFactory)
    expect(outcome.supported).toBe(false)
  })

  it('reports a truly unknown SimVSM class as unsupported with a distinct reason', () => {
    const node: SimVsmParsedNode = { key: '99', category: 'item', simvsmClass: 'brandNewSimvsmClass', nodeName: 'x', params: new Map() }
    const outcome = mapSimVsmNode(node, idFactory)
    expect(outcome.supported).toBe(false)
    expect(outcome.reasonDe).toContain('Unbekannte SimVSM-Klasse')
  })

  it('C17: a truly unknown SimVSM class gets a German generic PRIMARY label, never the raw internal class name — the class name only appears in reasonDe (details)', () => {
    const node: SimVsmParsedNode = { key: '99', category: 'item', simvsmClass: 'brandNewSimvsmClass', nodeName: 'x', params: new Map() }
    const outcome = mapSimVsmNode(node, idFactory)
    expect(outcome.labelDe).toBe('Nicht unterstütztes Element')
    expect(outcome.labelDe).not.toContain('brandNewSimvsmClass')
    expect(outcome.reasonDe).toContain('brandNewSimvsmClass')
  })

  it('C17 does NOT apply to a KNOWN-but-unsupported class — its real, maintained German label (e.g. noteVSM "Kaizen-Hinweis") is untouched', () => {
    const node = nodesOf().find((n) => n.key === '6')! // noteVSM in the shared fixture
    const outcome = mapSimVsmNode(node, idFactory)
    expect(outcome.labelDe).toBe('Kaizen-Hinweis')
  })

  it('treats a -1 "unlimited" sentinel on Capacity as absent, never as a negative inventoryMaxQuantity', () => {
    const node: SimVsmParsedNode = {
      key: '3',
      category: 'item',
      simvsmClass: 'inventory',
      nodeName: 'Lager',
      params: new Map([['maxCarrierNum', { class: 'maxCarrierNum', type: 'integer', value: -1 }]]),
    }
    const outcome = mapSimVsmNode(node, idFactory)
    expect(outcome.vsmNode?.inventoryMaxQuantity).toBeUndefined()
  })

  it('clamps an out-of-range Availability/ScrapRate into the 0–100 schema bound rather than rejecting the node', () => {
    const node: SimVsmParsedNode = {
      key: '2',
      category: 'item',
      simvsmClass: 'singleProcess',
      nodeName: 'x',
      params: new Map([
        ['Availability', { class: 'Availability', type: 'number', value: 140 }],
        ['ProductTable', { class: 'ProductTable', type: 'tableGrid', value: { rows: [{ ScrapRate: -5 }] } }],
      ]),
    }
    const outcome = mapSimVsmNode(node, idFactory)
    expect(outcome.vsmNode?.availabilityPct).toBe(100)
    expect(outcome.vsmNode?.scrapRate).toBeUndefined() // negative ScrapRate is guarded >=0 before clamp, so it's dropped, not clamped to 0-as-a-lie
  })

  it('every mapped node always has provenance "imported"', () => {
    for (const node of nodesOf()) {
      const outcome = mapSimVsmNode(node, idFactory)
      if (outcome.vsmNode) expect(outcome.vsmNode.provenance).toBe('imported')
    }
  })

  it('falls back to the mapping label when nodeName is blank', () => {
    const node: SimVsmParsedNode = { key: '2', category: 'item', simvsmClass: 'singleProcess', nodeName: '', params: new Map() }
    const outcome = mapSimVsmNode(node, idFactory)
    expect(outcome.vsmNode?.name).toBe('Prozess')
  })
})
