import { describe, it, expect } from 'vitest'
import type { VsmNode } from '@/lib/vsm-types'
import {
  NODE_W,
  getNodeH,
  getInputPortPos,
  getOutputPortPos,
  buildPath,
} from '@/components/wertstrom/vsm-geometry'
import { findBottleneckId, computeTimeline } from '@/components/wertstrom/vsm-metrics'
import { mapExcelRowsToPreview } from '@/components/wertstrom/vsm-excel'
import { NODE_CONFIG, PROVENANCE_CONFIDENCE_RANK, isProvenanceDowngrade } from '@/components/wertstrom/vsm-config'
import type { ValueProvenance } from '@/lib/vsm-types'

const mk = (id: string, patch: Partial<VsmNode> = {}): VsmNode => ({
  id,
  type: 'process',
  x: 0,
  y: 0,
  name: id,
  ...patch,
})

describe('vsm-geometry', () => {
  it('process and machine nodes are taller (110) than others (80)', () => {
    expect(getNodeH('process')).toBe(110)
    expect(getNodeH('machine')).toBe(110)
    expect(getNodeH('inventory')).toBe(80)
    expect(getNodeH('customer')).toBe(80)
  })

  it('output port is at the right edge, vertically centered', () => {
    const node = mk('a', { x: 50, y: 100, type: 'process' })
    expect(getOutputPortPos(node)).toEqual({ x: 50 + NODE_W, y: 100 + 55 })
  })

  it('input port is at the left edge, vertically centered', () => {
    const node = mk('a', { x: 50, y: 100, type: 'inventory' })
    expect(getInputPortPos(node)).toEqual({ x: 50, y: 100 + 40 })
  })

  it('buildPath enforces a minimum control-point distance of 80', () => {
    const near = buildPath(0, 0, 20, 0)
    expect(near).toBe('M 0 0 C 80 0, -60 0, 20 0')
  })

  it('buildPath scales control-point distance to 45% of horizontal gap', () => {
    const far = buildPath(0, 0, 1000, 100)
    expect(far).toBe('M 0 0 C 450 0, 550 100, 1000 100')
  })
})

describe('findBottleneckId', () => {
  it('returns null when fewer than two process nodes have a cycle time', () => {
    expect(findBottleneckId([])).toBeNull()
    expect(findBottleneckId([mk('a', { cycleTimeSec: 30 })])).toBeNull()
  })

  it('returns the id of the slowest process node', () => {
    const nodes: VsmNode[] = [
      mk('a', { cycleTimeSec: 30 }),
      mk('b', { cycleTimeSec: 60 }),
      mk('c', { cycleTimeSec: 45 }),
    ]
    expect(findBottleneckId(nodes)).toBe('b')
  })

  it('ignores non-process node types even if they have a cycleTime', () => {
    const nodes: VsmNode[] = [
      mk('a', { cycleTimeSec: 30 }),
      mk('b', { cycleTimeSec: 90, type: 'machine' }),
      mk('c', { cycleTimeSec: 45 }),
    ]
    expect(findBottleneckId(nodes)).toBe('c')
  })

  it('ignores process nodes whose cycleTime is 0 or missing', () => {
    const nodes: VsmNode[] = [
      mk('a', { cycleTimeSec: 30 }),
      mk('b'),
      mk('c', { cycleTimeSec: 0 }),
    ]
    expect(findBottleneckId(nodes)).toBeNull()
  })
})

describe('computeTimeline', () => {
  it('returns all zeros when node list is empty', () => {
    expect(computeTimeline([])).toEqual({ va: 0, nva: 0, total: 0, pct: 0 })
  })

  it('sums only cycleTime from value-added nodes into va', () => {
    const nodes: VsmNode[] = [
      mk('a', { isValueAdded: true, cycleTimeSec: 30 }),
      mk('b', { isValueAdded: true, cycleTimeSec: 20 }),
    ]
    const r = computeTimeline(nodes)
    expect(r.va).toBe(50)
    expect(r.nva).toBe(0)
    expect(r.pct).toBe(100)
  })

  it('sums cycleTime + waitTime from non-value-added nodes into nva (excluding customer/supplier)', () => {
    const nodes: VsmNode[] = [
      mk('a', { isValueAdded: true, cycleTimeSec: 60 }),
      mk('b', { isValueAdded: false, type: 'inventory', cycleTimeSec: 10, waitTimeSec: 30 }),
      mk('c', { isValueAdded: false, type: 'customer', cycleTimeSec: 999 }),
      mk('d', { isValueAdded: false, type: 'supplier', cycleTimeSec: 999 }),
    ]
    const r = computeTimeline(nodes)
    expect(r.va).toBe(60)
    expect(r.nva).toBe(40)
    expect(r.total).toBe(100)
    expect(r.pct).toBe(60)
  })

  it('rounds pct to the nearest integer', () => {
    const nodes: VsmNode[] = [
      mk('a', { isValueAdded: true, cycleTimeSec: 1 }),
      mk('b', { isValueAdded: false, type: 'inventory', cycleTimeSec: 2 }),
    ]
    expect(computeTimeline(nodes).pct).toBe(33)
  })
})

describe('mapExcelRowsToPreview', () => {
  it('reads the canonical column names', () => {
    const rows = [
      {
        Prozessschritt: 'Pressen',
        Zykluszeit: 45,
        Maschinenzeit: 40,
        'Manuelle Zeit': 5,
        Rüstzeit: 120,
        Wartezeit: 10,
      },
    ]
    expect(mapExcelRowsToPreview(rows)).toEqual([
      {
        name: 'Pressen',
        cycleTimeSec: 45,
        machineTimeSec: 40,
        manualTimeSec: 5,
        setupTimeSec: 120,
        waitTimeSec: 10,
      },
    ])
  })

  it('falls back to "Name" column when "Prozessschritt" is missing', () => {
    expect(mapExcelRowsToPreview([{ Name: 'Fügen' }])[0].name).toBe('Fügen')
  })

  // Review-Fix F3 (PR #351): a missing cell is UNKNOWN (undefined), not 0 —
  // the old "coerce to 0" behavior fabricated data ("0s Rüstzeit" is real
  // input, an empty cell is not). Detail coverage incl. null/whitespace/0
  // lives in components/wertstrom/__tests__/vsm-excel.test.ts.
  it('maps missing numeric columns to undefined, not 0', () => {
    expect(mapExcelRowsToPreview([{ Prozessschritt: 'A' }])[0]).toEqual({
      name: 'A',
      cycleTimeSec: undefined,
      machineTimeSec: undefined,
      manualTimeSec: undefined,
      setupTimeSec: undefined,
      waitTimeSec: undefined,
    })
  })

  it('drops rows with an empty name', () => {
    const rows = [
      { Prozessschritt: '', Zykluszeit: 10 },
      { Prozessschritt: 'Pressen', Zykluszeit: 20 },
    ]
    const out = mapExcelRowsToPreview(rows)
    expect(out).toHaveLength(1)
    expect(out[0].name).toBe('Pressen')
  })
})

// Review-Fix F7 (PR #353 adversarial review): confidence-order helper for
// node.provenance, used to guard against a silent downgrade (e.g. the
// Stoppuhr-Picker's 'planned' overwriting an already-'measured' value).
describe('isProvenanceDowngrade', () => {
  it('is never a downgrade when the current provenance is absent (unknown)', () => {
    const allProvenances: ValueProvenance[] = ['planned', 'measured', 'calculated', 'imported', 'assumed']
    for (const next of allProvenances) {
      expect(isProvenanceDowngrade(undefined, next)).toBe(false)
    }
  })

  it('measured -> planned IS a downgrade (the core case the review-fix guards)', () => {
    expect(isProvenanceDowngrade('measured', 'planned')).toBe(true)
  })

  it('measured -> anything else is always a downgrade or equal, never an upgrade (measured is top rank)', () => {
    expect(isProvenanceDowngrade('measured', 'measured')).toBe(false)
    expect(isProvenanceDowngrade('measured', 'calculated')).toBe(true)
    expect(isProvenanceDowngrade('measured', 'imported')).toBe(true)
    expect(isProvenanceDowngrade('measured', 'assumed')).toBe(true)
  })

  it('planned -> measured is an UPGRADE, not a downgrade', () => {
    expect(isProvenanceDowngrade('planned', 'measured')).toBe(false)
  })

  it('calculated <-> imported are tied rank — neither direction is a downgrade', () => {
    expect(isProvenanceDowngrade('calculated', 'imported')).toBe(false)
    expect(isProvenanceDowngrade('imported', 'calculated')).toBe(false)
  })

  it('re-asserting the SAME provenance is never a downgrade', () => {
    expect(isProvenanceDowngrade('assumed', 'assumed')).toBe(false)
  })

  it('PROVENANCE_CONFIDENCE_RANK declares all 5 ValueProvenance values', () => {
    const expected: ValueProvenance[] = ['planned', 'measured', 'calculated', 'imported', 'assumed']
    for (const p of expected) {
      expect(PROVENANCE_CONFIDENCE_RANK[p]).toBeGreaterThan(0)
    }
  })
})

describe('NODE_CONFIG', () => {
  it('declares all seven node types', () => {
    const expected: Array<keyof typeof NODE_CONFIG> = [
      'process',
      'machine',
      'inventory',
      'transport',
      'customer',
      'supplier',
      'timevalue',
    ]
    for (const k of expected) {
      expect(NODE_CONFIG[k]).toBeDefined()
      expect(NODE_CONFIG[k].label).toBeTruthy()
      // Wertstrom P0 (KAR-878): colors moved from raw hex to CSS variables
      // (app/globals.css --vsm-node-*, see CLAUDE.md "BMW Color System"
      // exception entry) — light-mode values behind the vars are unchanged.
      expect(NODE_CONFIG[k].color).toMatch(/^var\(--vsm-node-[a-z]+\)$/)
      expect(NODE_CONFIG[k].icon).toBeTruthy()
    }
  })
})
