// Wertstrom (VSM) Zod-Schema-Tests (Wertstrom P0, KAR-878) — app/api/wertstrom/
// route.ts (GET/POST) und [id]/route.ts (PUT/DELETE) validieren jetzt über
// diese Schemas statt reiner `as`-Casts. Deckt die im Task genannten
// Anforderungen ab: Zeiten/Mengen >= 0, oee/scrapRate 0–100, title non-empty
// bei POST, Referenz-Integrität (Connection -> Node im selben Payload),
// .passthrough()-Toleranz gegenüber unbekannten Zusatzfeldern, null/undefined
// bei optionalen Feldern.
//
// FIXTURE-DATEN-REGEL: every id/name/number below is FREE INVENTION.

import { describe, it, expect } from 'vitest'
import {
  VsmNodeSchema,
  VsmConnectionSchema,
  VsmLayoutSchema,
  CreateValueStreamMapBody,
  UpdateValueStreamMapBody,
} from '../schemas'

function baseNode(overrides: Record<string, unknown> = {}) {
  return { id: 'n1', type: 'process', x: 0, y: 0, name: 'Schweißen', ...overrides }
}

describe('VsmNodeSchema', () => {
  it('accepts a minimal node (only the required fields)', () => {
    expect(VsmNodeSchema.safeParse(baseNode()).success).toBe(true)
  })

  it('rejects a negative time/quantity field', () => {
    for (const field of ['cycleTimeSec', 'machineTimeSec', 'setupTimeSec', 'waitTimeSec', 'quantity', 'distance', 'demand', 'numWorkers']) {
      const result = VsmNodeSchema.safeParse(baseNode({ [field]: -1 }))
      expect(result.success, `${field}: -1 should be rejected`).toBe(false)
    }
  })

  it('accepts 0 for a time/quantity field (boundary, not "no data")', () => {
    expect(VsmNodeSchema.safeParse(baseNode({ cycleTimeSec: 0 })).success).toBe(true)
  })

  it('rejects oee/scrapRate outside 0–100', () => {
    expect(VsmNodeSchema.safeParse(baseNode({ oee: -1 })).success).toBe(false)
    expect(VsmNodeSchema.safeParse(baseNode({ oee: 101 })).success).toBe(false)
    expect(VsmNodeSchema.safeParse(baseNode({ scrapRate: -1 })).success).toBe(false)
    expect(VsmNodeSchema.safeParse(baseNode({ scrapRate: 101 })).success).toBe(false)
  })

  it('accepts oee/scrapRate at the 0 and 100 boundaries', () => {
    expect(VsmNodeSchema.safeParse(baseNode({ oee: 0 })).success).toBe(true)
    expect(VsmNodeSchema.safeParse(baseNode({ oee: 100 })).success).toBe(true)
    expect(VsmNodeSchema.safeParse(baseNode({ scrapRate: 0 })).success).toBe(true)
    expect(VsmNodeSchema.safeParse(baseNode({ scrapRate: 100 })).success).toBe(true)
  })

  it('tolerates null for optional fields (not just undefined/absent)', () => {
    const result = VsmNodeSchema.safeParse(baseNode({ cycleTimeSec: null, notes: null, machineType: null }))
    expect(result.success).toBe(true)
  })

  it('rejects an unknown node type', () => {
    expect(VsmNodeSchema.safeParse(baseNode({ type: 'not-a-type' })).success).toBe(false)
  })

  // Wertstrom P2 (A8/A12, KAR-878/KAR-986)
  it('accepts the P2 inventoryKind/inventoryMaxQuantity/provenance fields', () => {
    const result = VsmNodeSchema.safeParse(
      baseNode({ type: 'inventory', inventoryKind: 'fifo', inventoryMaxQuantity: 20, provenance: 'measured' }),
    )
    expect(result.success).toBe(true)
  })

  it('rejects an unknown inventoryKind/provenance value', () => {
    expect(VsmNodeSchema.safeParse(baseNode({ inventoryKind: 'not-a-kind' })).success).toBe(false)
    expect(VsmNodeSchema.safeParse(baseNode({ provenance: 'not-a-provenance' })).success).toBe(false)
  })

  it('rejects a negative inventoryMaxQuantity', () => {
    expect(VsmNodeSchema.safeParse(baseNode({ inventoryMaxQuantity: -1 })).success).toBe(false)
  })

  // Review-Fix F14 (adversarial review, PR #353): 0 = a real, deliberately
  // set "no spare capacity" value; absent = unknown/not set. The project's
  // own undefined-doctrine makes this distinction load-bearing — an explicit
  // 0 must round-trip as 0, never get coerced to/confused with absent.
  it('F14 FIX: an explicit inventoryMaxQuantity of 0 round-trips as 0, not undefined/absent', () => {
    const result = VsmNodeSchema.safeParse(baseNode({ type: 'inventory', inventoryKind: 'fifo', inventoryMaxQuantity: 0 }))
    expect(result.success).toBe(true)
    if (result.success) expect(result.data.inventoryMaxQuantity).toBe(0)
  })

  it('P2 fields default absent (fehlendes Feld = heutige Semantik) — a bare node is unaffected', () => {
    const result = VsmNodeSchema.safeParse(baseNode())
    expect(result.success).toBe(true)
    if (result.success) {
      expect(result.data.inventoryKind).toBeUndefined()
      expect(result.data.provenance).toBeUndefined()
    }
  })

  it('passes through unknown fields (forward-compat with later QVS phases)', () => {
    const result = VsmNodeSchema.safeParse(baseNode({ aFutureField: 'x' }))
    expect(result.success).toBe(true)
    if (result.success) expect((result.data as Record<string, unknown>).aFutureField).toBe('x')
  })

  it('accepts a full QAF-import qafSource block, including its own passthrough fields', () => {
    const result = VsmNodeSchema.safeParse(
      baseNode({
        vaClass: 'unknown',
        fieldStatus: { name: 'imported', cycleTimeSec: 'modified' },
        qafSource: {
          importId: 'import-1',
          rowIndex: 0,
          fields: { zykluszeit: { original: 45, unit: 's', aFutureQafField: 'x' } },
        },
      }),
    )
    expect(result.success).toBe(true)
  })

  it('rejects a qafSource missing its required importId/rowIndex', () => {
    expect(VsmNodeSchema.safeParse(baseNode({ qafSource: { fields: {} } })).success).toBe(false)
  })

  // Wertstrom P4 (B3/B4, Capability-Matrix, KAR-878/KAR-986)
  it('accepts the P4 availabilityPct/mtbfMin/mttrMin/transportFrequency fields', () => {
    const result = VsmNodeSchema.safeParse(baseNode({ type: 'transport', availabilityPct: 92.5, mtbfMin: 90, mttrMin: 10, transportFrequency: '3x täglich' }))
    expect(result.success).toBe(true)
  })

  it('rejects availabilityPct outside 0–100', () => {
    expect(VsmNodeSchema.safeParse(baseNode({ availabilityPct: -1 })).success).toBe(false)
    expect(VsmNodeSchema.safeParse(baseNode({ availabilityPct: 101 })).success).toBe(false)
  })

  it('accepts availabilityPct at the 0 and 100 boundaries', () => {
    expect(VsmNodeSchema.safeParse(baseNode({ availabilityPct: 0 })).success).toBe(true)
    expect(VsmNodeSchema.safeParse(baseNode({ availabilityPct: 100 })).success).toBe(true)
  })

  it('rejects a negative mtbfMin/mttrMin', () => {
    expect(VsmNodeSchema.safeParse(baseNode({ mtbfMin: -1 })).success).toBe(false)
    expect(VsmNodeSchema.safeParse(baseNode({ mttrMin: -1 })).success).toBe(false)
  })

  it('P4 fields default absent — a bare node is unaffected', () => {
    const result = VsmNodeSchema.safeParse(baseNode())
    expect(result.success).toBe(true)
    if (result.success) {
      expect(result.data.availabilityPct).toBeUndefined()
      expect(result.data.mtbfMin).toBeUndefined()
      expect(result.data.mttrMin).toBeUndefined()
      expect(result.data.transportFrequency).toBeUndefined()
    }
  })

  it('tolerates null for the P4 fields (not just undefined/absent)', () => {
    const result = VsmNodeSchema.safeParse(baseNode({ availabilityPct: null, mtbfMin: null, mttrMin: null, transportFrequency: null }))
    expect(result.success).toBe(true)
  })

  // Wertstrom P8.1 (A18, KAR-878/KAR-986, execution-prompt §15.5 "Measure Management")
  it('accepts the P8.1 kaizenNote/measureRefs fields', () => {
    const result = VsmNodeSchema.safeParse(baseNode({ kaizenNote: 'Engpass entschärfen', measureRefs: ['m1', 'm2'] }))
    expect(result.success).toBe(true)
    if (result.success) {
      expect(result.data.kaizenNote).toBe('Engpass entschärfen')
      expect(result.data.measureRefs).toEqual(['m1', 'm2'])
    }
  })

  it('accepts an empty-string kaizenNote (presence of the field IS the marker, not the text length)', () => {
    expect(VsmNodeSchema.safeParse(baseNode({ kaizenNote: '' })).success).toBe(true)
  })

  it('accepts an empty measureRefs array (marked but no measure created yet)', () => {
    const result = VsmNodeSchema.safeParse(baseNode({ measureRefs: [] }))
    expect(result.success).toBe(true)
    if (result.success) expect(result.data.measureRefs).toEqual([])
  })

  it('rejects an empty-string entry inside measureRefs', () => {
    expect(VsmNodeSchema.safeParse(baseNode({ measureRefs: [''] })).success).toBe(false)
  })

  it('P8.1 fields default absent — a bare node is unaffected', () => {
    const result = VsmNodeSchema.safeParse(baseNode())
    expect(result.success).toBe(true)
    if (result.success) {
      expect(result.data.kaizenNote).toBeUndefined()
      expect(result.data.measureRefs).toBeUndefined()
    }
  })

  it('tolerates null for the P8.1 fields (not just undefined/absent)', () => {
    const result = VsmNodeSchema.safeParse(baseNode({ kaizenNote: null, measureRefs: null }))
    expect(result.success).toBe(true)
  })

  // Wertstrom P8.2a (Baustein 2 "PPS als echtes Datenfeld", KAR-878/KAR-986)
  it('accepts isPps: true', () => {
    const result = VsmNodeSchema.safeParse(baseNode({ isPps: true }))
    expect(result.success).toBe(true)
    if (result.success) expect(result.data.isPps).toBe(true)
  })

  it('rejects isPps: false — the field is true-only (sparse "presence IS the marker", never an explicit false)', () => {
    expect(VsmNodeSchema.safeParse(baseNode({ isPps: false })).success).toBe(false)
  })

  it('isPps defaults absent — a bare node is unaffected', () => {
    const result = VsmNodeSchema.safeParse(baseNode())
    expect(result.success).toBe(true)
    if (result.success) expect(result.data.isPps).toBeUndefined()
  })

  it('tolerates null for isPps (not just undefined/absent)', () => {
    expect(VsmNodeSchema.safeParse(baseNode({ isPps: null })).success).toBe(true)
  })
})

describe('VsmConnectionSchema', () => {
  it('accepts a minimal connection', () => {
    expect(VsmConnectionSchema.safeParse({ id: 'c1', fromNodeId: 'n1', toNodeId: 'n2' }).success).toBe(true)
  })

  it('rejects a negative transportTimeSec/batchSize', () => {
    expect(VsmConnectionSchema.safeParse({ id: 'c1', fromNodeId: 'n1', toNodeId: 'n2', transportTimeSec: -1 }).success).toBe(false)
    expect(VsmConnectionSchema.safeParse({ id: 'c1', fromNodeId: 'n1', toNodeId: 'n2', batchSize: -1 }).success).toBe(false)
  })

  it('rejects an empty fromNodeId/toNodeId', () => {
    expect(VsmConnectionSchema.safeParse({ id: 'c1', fromNodeId: '', toNodeId: 'n2' }).success).toBe(false)
  })

  // Wertstrom P2 (A7, KAR-878/KAR-986)
  it('accepts the P2 kind/frequency fields', () => {
    const result = VsmConnectionSchema.safeParse({ id: 'c1', fromNodeId: 'n1', toNodeId: 'n2', kind: 'information', frequency: 'täglich' })
    expect(result.success).toBe(true)
  })

  it('rejects an unknown kind value', () => {
    expect(VsmConnectionSchema.safeParse({ id: 'c1', fromNodeId: 'n1', toNodeId: 'n2', kind: 'not-a-kind' }).success).toBe(false)
  })

  it('kind defaults absent (fehlendes Feld = materialFlow) — a bare connection is unaffected', () => {
    const result = VsmConnectionSchema.safeParse({ id: 'c1', fromNodeId: 'n1', toNodeId: 'n2' })
    expect(result.success).toBe(true)
    if (result.success) expect(result.data.kind).toBeUndefined()
  })

  // Wertstrom P8.2a (Baustein 3 "Push/Pull/Kanban-Kantensemantik", KAR-878/KAR-986)
  it('accepts flowControl push/pull/kanban on a materialFlow connection (kind absent)', () => {
    for (const flowControl of ['push', 'pull', 'kanban'] as const) {
      const result = VsmConnectionSchema.safeParse({ id: 'c1', fromNodeId: 'n1', toNodeId: 'n2', flowControl })
      expect(result.success, flowControl).toBe(true)
      if (result.success) expect(result.data.flowControl).toBe(flowControl)
    }
  })

  it('accepts flowControl on a connection with an EXPLICIT kind: "materialFlow"', () => {
    expect(VsmConnectionSchema.safeParse({ id: 'c1', fromNodeId: 'n1', toNodeId: 'n2', kind: 'materialFlow', flowControl: 'kanban' }).success).toBe(true)
  })

  it('rejects an unknown flowControl value', () => {
    expect(VsmConnectionSchema.safeParse({ id: 'c1', fromNodeId: 'n1', toNodeId: 'n2', flowControl: 'not-a-value' }).success).toBe(false)
  })

  it('rejects flowControl on an information-kind connection (nur materialFlow sinnvoll)', () => {
    const result = VsmConnectionSchema.safeParse({ id: 'c1', fromNodeId: 'n1', toNodeId: 'n2', kind: 'information', flowControl: 'push' })
    expect(result.success).toBe(false)
  })

  it('flowControl defaults absent — a bare connection is unaffected', () => {
    const result = VsmConnectionSchema.safeParse({ id: 'c1', fromNodeId: 'n1', toNodeId: 'n2' })
    expect(result.success).toBe(true)
    if (result.success) expect(result.data.flowControl).toBeUndefined()
  })

  it('tolerates null for flowControl (not just undefined/absent)', () => {
    expect(VsmConnectionSchema.safeParse({ id: 'c1', fromNodeId: 'n1', toNodeId: 'n2', flowControl: null }).success).toBe(true)
  })
})

describe('VsmLayoutSchema', () => {
  it('accepts the pre-existing DB default ({})', () => {
    expect(VsmLayoutSchema.safeParse({}).success).toBe(true)
  })

  it('accepts a valid viewport', () => {
    expect(VsmLayoutSchema.safeParse({ viewport: { x: 1, y: 2, zoom: 0.5 } }).success).toBe(true)
  })

  it('tolerates unrelated sibling keys at the layout root (forward-compat)', () => {
    expect(VsmLayoutSchema.safeParse({ viewport: { x: 1, y: 2, zoom: 1 }, aFutureLayoutField: true }).success).toBe(true)
  })

  it('tolerates a legacy/foreign layout shape without "viewport" (e.g. a demo seed\'s flat { zoom, panX, panY })', () => {
    expect(VsmLayoutSchema.safeParse({ zoom: 0.75, panX: 0, panY: 0 }).success).toBe(true)
  })

  it('rejects a viewport with the wrong field types', () => {
    expect(VsmLayoutSchema.safeParse({ viewport: { x: '1', y: 2, zoom: 1 } }).success).toBe(false)
  })

  // Wertstrom P4 (B2, Capability-Matrix, KAR-878/KAR-986)
  it('accepts a valid shiftModel (no optional plannedDowntimeMinPerShift)', () => {
    expect(VsmLayoutSchema.safeParse({ shiftModel: { hoursPerShift: 8, shiftsPerDay: 2, breakMinPerShift: 30 } }).success).toBe(true)
  })

  it('accepts a valid shiftModel including plannedDowntimeMinPerShift', () => {
    expect(
      VsmLayoutSchema.safeParse({ shiftModel: { hoursPerShift: 8, shiftsPerDay: 1, breakMinPerShift: 15, plannedDowntimeMinPerShift: 10 } }).success,
    ).toBe(true)
  })

  it('accepts viewport and shiftModel together', () => {
    expect(
      VsmLayoutSchema.safeParse({ viewport: { x: 1, y: 2, zoom: 1 }, shiftModel: { hoursPerShift: 8, shiftsPerDay: 2, breakMinPerShift: 30 } }).success,
    ).toBe(true)
  })

  it('rejects a shiftModel missing a required field', () => {
    expect(VsmLayoutSchema.safeParse({ shiftModel: { hoursPerShift: 8, shiftsPerDay: 2 } }).success).toBe(false)
  })

  it('rejects a negative shiftModel field', () => {
    expect(VsmLayoutSchema.safeParse({ shiftModel: { hoursPerShift: -1, shiftsPerDay: 2, breakMinPerShift: 30 } }).success).toBe(false)
  })

  it('rejects a shiftModel with the wrong field types', () => {
    expect(VsmLayoutSchema.safeParse({ shiftModel: { hoursPerShift: '8', shiftsPerDay: 2, breakMinPerShift: 30 } }).success).toBe(false)
  })

  // Review-Fix F3+F5 (adversarial review, PR #355): explicit `null` is how
  // a client signals "delete the saved shiftModel" — distinct from simply
  // omitting the key (still accepted, tested above via `{}` and via a
  // viewport-only body). See route.ts's merge logic for how the two are
  // told apart server-side.
  it('accepts an explicit null shiftModel (Review-Fix F3+F5 — signals delete)', () => {
    const result = VsmLayoutSchema.safeParse({ viewport: { x: 1, y: 2, zoom: 1 }, shiftModel: null })
    expect(result.success).toBe(true)
    if (result.success) expect(result.data.shiftModel).toBeNull()
  })

  it('a shiftModel key that is simply absent still parses to undefined, not null (the two stay distinguishable)', () => {
    const result = VsmLayoutSchema.safeParse({ viewport: { x: 1, y: 2, zoom: 1 } })
    expect(result.success).toBe(true)
    if (result.success) expect(result.data.shiftModel).toBeUndefined()
  })

  // Review-Fix F9 (adversarial review, PR #355): a POSITIVE but implausible
  // value (e.g. an 80h shift, likely a typo for 8h) used to sail through —
  // only `.min(0)` existed. Bounds: hoursPerShift ≤ 24, shiftsPerDay ≤ 6,
  // breakMinPerShift/plannedDowntimeMinPerShift ≤ 1440 (a full day, in
  // minutes).
  describe('Review-Fix F9: shiftModel plausibility ceilings', () => {
    it('accepts values exactly AT each ceiling', () => {
      expect(
        VsmLayoutSchema.safeParse({
          shiftModel: { hoursPerShift: 24, shiftsPerDay: 6, breakMinPerShift: 1440, plannedDowntimeMinPerShift: 1440 },
        }).success,
      ).toBe(true)
    })

    it('rejects hoursPerShift above 24', () => {
      expect(VsmLayoutSchema.safeParse({ shiftModel: { hoursPerShift: 25, shiftsPerDay: 2, breakMinPerShift: 30 } }).success).toBe(false)
    })

    it('rejects shiftsPerDay above 6', () => {
      expect(VsmLayoutSchema.safeParse({ shiftModel: { hoursPerShift: 8, shiftsPerDay: 7, breakMinPerShift: 30 } }).success).toBe(false)
    })

    it('rejects breakMinPerShift above 1440', () => {
      expect(VsmLayoutSchema.safeParse({ shiftModel: { hoursPerShift: 8, shiftsPerDay: 2, breakMinPerShift: 1441 } }).success).toBe(false)
    })

    it('rejects plannedDowntimeMinPerShift above 1440', () => {
      expect(
        VsmLayoutSchema.safeParse({ shiftModel: { hoursPerShift: 8, shiftsPerDay: 2, breakMinPerShift: 30, plannedDowntimeMinPerShift: 1441 } })
          .success,
      ).toBe(false)
    })
  })

  // VSM-Standard-Visualisierung, Baustein 2 (Kais-Live-Feedback 23.07.,
  // KAR-878): timelineOffsetY — sparse, simple numeric UI-preference (the
  // canvas timeline band's vertical nudge). Simpler contract than
  // shiftModel above (only .optional(), no .nullable() three-way) — see
  // VsmLayoutSchema's own doc comment for why.
  describe('timelineOffsetY', () => {
    it('accepts a valid finite number (positive, negative, and zero)', () => {
      expect(VsmLayoutSchema.safeParse({ timelineOffsetY: 42 }).success).toBe(true)
      expect(VsmLayoutSchema.safeParse({ timelineOffsetY: -12.5 }).success).toBe(true)
      expect(VsmLayoutSchema.safeParse({ timelineOffsetY: 0 }).success).toBe(true)
    })

    it('a timelineOffsetY key that is simply absent still parses to undefined (this save does not touch it)', () => {
      const result = VsmLayoutSchema.safeParse({ viewport: { x: 1, y: 2, zoom: 1 } })
      expect(result.success).toBe(true)
      if (result.success) expect(result.data.timelineOffsetY).toBeUndefined()
    })

    it('rejects a non-number value', () => {
      expect(VsmLayoutSchema.safeParse({ timelineOffsetY: '5' }).success).toBe(false)
    })

    it('rejects an explicit null (unlike shiftModel, timelineOffsetY has no "delete" semantics — 0 is always a valid reset)', () => {
      expect(VsmLayoutSchema.safeParse({ timelineOffsetY: null }).success).toBe(false)
    })

    it('accepts viewport, shiftModel and timelineOffsetY together', () => {
      expect(
        VsmLayoutSchema.safeParse({
          viewport: { x: 1, y: 2, zoom: 1 },
          shiftModel: { hoursPerShift: 8, shiftsPerDay: 2, breakMinPerShift: 30 },
          timelineOffsetY: 16,
        }).success,
      ).toBe(true)
    })

    // K4-Fix (Referenzwertstrom-Fixrunde 2, KAR-878): before this fix,
    // timelineOffsetY had no bound at all — a request body could carry an
    // arbitrarily large value straight through to the DB. This schema-level
    // bound is a DELIBERATELY GENEROUS ±1,000,000 sanity ceiling, not the
    // tight semantic clamp — an old-client "giant value" like the finding's
    // own `100000` example must still PASS here (route.ts's merge is what
    // clamps it down to the real -12/2000 range via resolveTimelineOffsetY,
    // gracefully, instead of 400ing the entire save over one stale
    // UI-preference field) — see VsmLayoutSchema's own doc comment.
    it('K4-Fix: accepts a large-but-plausible legacy value (an old client that predates the real semantic clamp) — schema is a sanity ceiling, not the tight clamp', () => {
      expect(VsmLayoutSchema.safeParse({ timelineOffsetY: 100000 }).success).toBe(true)
      expect(VsmLayoutSchema.safeParse({ timelineOffsetY: -50000 }).success).toBe(true)
    })

    it('K4-Fix: accepts the exact ±1,000,000 sanity-ceiling boundary', () => {
      expect(VsmLayoutSchema.safeParse({ timelineOffsetY: 1_000_000 }).success).toBe(true)
      expect(VsmLayoutSchema.safeParse({ timelineOffsetY: -1_000_000 }).success).toBe(true)
    })

    it('K4-Fix: rejects a genuinely pathological/adversarial value beyond the sanity ceiling', () => {
      expect(VsmLayoutSchema.safeParse({ timelineOffsetY: 1_000_000.01 }).success).toBe(false)
      expect(VsmLayoutSchema.safeParse({ timelineOffsetY: -1_000_000.01 }).success).toBe(false)
      expect(VsmLayoutSchema.safeParse({ timelineOffsetY: 1e15 }).success).toBe(false)
    })
  })
})

describe('CreateValueStreamMapBody (POST /api/wertstrom)', () => {
  it('accepts a minimal valid body (title only)', () => {
    expect(CreateValueStreamMapBody.safeParse({ title: 'Wertstrom Linie 3' }).success).toBe(true)
  })

  it('rejects an empty or whitespace-only title', () => {
    expect(CreateValueStreamMapBody.safeParse({ title: '' }).success).toBe(false)
    expect(CreateValueStreamMapBody.safeParse({ title: '   ' }).success).toBe(false)
  })

  it('rejects a missing title', () => {
    expect(CreateValueStreamMapBody.safeParse({}).success).toBe(false)
  })

  it('tolerates null description/project_id', () => {
    expect(CreateValueStreamMapBody.safeParse({ title: 'x', description: null, project_id: null }).success).toBe(true)
  })

  it('rejects a project_id that is not a UUID', () => {
    expect(CreateValueStreamMapBody.safeParse({ title: 'x', project_id: 'not-a-uuid' }).success).toBe(false)
  })
})

describe('UpdateValueStreamMapBody (PUT /api/wertstrom/[id]) — referential integrity', () => {
  it('accepts nodes+connections where every connection resolves within the payload', () => {
    const result = UpdateValueStreamMapBody.safeParse({
      nodes: [baseNode({ id: 'n1' }), baseNode({ id: 'n2' })],
      connections: [{ id: 'c1', fromNodeId: 'n1', toNodeId: 'n2' }],
    })
    expect(result.success).toBe(true)
  })

  it('rejects a connection whose fromNodeId is not in the payload\'s nodes', () => {
    const result = UpdateValueStreamMapBody.safeParse({
      nodes: [baseNode({ id: 'n2' })],
      connections: [{ id: 'c1', fromNodeId: 'n-missing', toNodeId: 'n2' }],
    })
    expect(result.success).toBe(false)
  })

  it('rejects a connection whose toNodeId is not in the payload\'s nodes', () => {
    const result = UpdateValueStreamMapBody.safeParse({
      nodes: [baseNode({ id: 'n1' })],
      connections: [{ id: 'c1', fromNodeId: 'n1', toNodeId: 'n-missing' }],
    })
    expect(result.success).toBe(false)
  })

  it('rejects any connection when nodes is an empty array (nothing to resolve against)', () => {
    const result = UpdateValueStreamMapBody.safeParse({
      nodes: [],
      connections: [{ id: 'c1', fromNodeId: 'n1', toNodeId: 'n2' }],
    })
    expect(result.success).toBe(false)
  })

  it('rejects a connections-only payload with no nodes at all — bypass fix (Review-Fix F5)', () => {
    const result = UpdateValueStreamMapBody.safeParse({
      connections: [{ id: 'c1', fromNodeId: 'n1', toNodeId: 'n2' }],
    })
    expect(result.success).toBe(false)
    if (!result.success) expect(result.error.issues[0].message).toBe('connections requires nodes')
  })

  it('skips the referential check when nodes is present but connections is absent', () => {
    const result = UpdateValueStreamMapBody.safeParse({ nodes: [baseNode()] })
    expect(result.success).toBe(true)
  })

  it('accepts a body with no nodes/connections at all (e.g. a title-only rename)', () => {
    expect(UpdateValueStreamMapBody.safeParse({ title: 'Neuer Titel' }).success).toBe(true)
  })

  it('rejects an empty title when explicitly provided (non-empty only applies to POST per the task, but an explicit empty rename is still nonsensical)', () => {
    expect(UpdateValueStreamMapBody.safeParse({ title: '' }).success).toBe(false)
  })

  it('accepts layout.viewport alongside nodes/connections', () => {
    const result = UpdateValueStreamMapBody.safeParse({
      nodes: [baseNode({ id: 'n1' })],
      connections: [],
      layout: { viewport: { x: 10, y: 20, zoom: 1.5 } },
    })
    expect(result.success).toBe(true)
  })
})
