/**
 * Mapping-drift guard (QVS-P1, KAR-970): reads
 * mappings/qaf-to-value-stream-mapping.yaml (the versioned, verbindlich field
 * contract — see reports/qaf-value-stream-field-mapping.md) with a small
 * hand-rolled line-based parser (no new dependency — the file has a fixed,
 * simple shape) and cross-checks it against the mapper's actual constants
 * and runtime behavior.
 *
 * The parser is deliberately NOT a flat "grab every `source:`/`target:` in
 * the section" regex scan: two of the block-style entries (zykluszeit,
 * ausschuss) carry a nested `unit: { source: s, target: s, conversion: none }`
 * flow-map that ALSO contains the literal substrings "source:" and "target:"
 * — a flat scan would silently pick those up and misalign every pair after
 * them. Instead this walks the section line by line and only ever captures
 * a key when it is the line's OWN first token (after trimming), which
 * excludes anything nested inside another key's flow-map value.
 */

import { describe, it, expect } from 'vitest'
import { readFileSync } from 'node:fs'
import path from 'node:path'
import { MAPPING_VERSION, PRIMARY_FIELD_MAPPINGS, METADATA_ONLY_FIELDS, mapQafRowsToVsmNodes } from '../mapper'
import { buildQafRow } from './fixtures'

const YAML_PATH = path.join(process.cwd(), 'mappings/qaf-to-value-stream-mapping.yaml')

interface YamlMappingEntry {
  source: string
  target: string
}

function parseMappingYaml(raw: string): { version: string; mappings: YamlMappingEntry[] } {
  const versionMatch = raw.match(/^version:\s*(\S+)/m)
  if (!versionMatch) throw new Error('mapping YAML: no top-level version: found')
  const version = versionMatch[1]

  const lines = raw.split('\n')
  const mappingsIdx = lines.findIndex((l) => l === 'mappings:')
  if (mappingsIdx === -1) throw new Error('mapping YAML: no top-level mappings: key found')

  let endIdx = lines.length
  for (let i = mappingsIdx + 1; i < lines.length; i++) {
    if (/^[a-zA-Z_]+:/.test(lines[i])) {
      endIdx = i
      break
    }
  }
  const section = lines.slice(mappingsIdx + 1, endIdx)

  const mappings: YamlMappingEntry[] = []
  let currentSource: string | null = null
  let currentTarget: string | null = null

  function flush(): void {
    if (currentSource !== null && currentTarget !== null) {
      mappings.push({ source: currentSource, target: currentTarget })
    } else if (currentSource !== null || currentTarget !== null) {
      throw new Error(`mapping YAML: dangling entry source="${currentSource}" target="${currentTarget}" (missing counterpart)`)
    }
    currentSource = null
    currentTarget = null
  }

  for (const line of section) {
    const trimmed = line.trim()
    if (trimmed === '' || trimmed.startsWith('#')) continue

    if (trimmed.startsWith('- {')) {
      // Flow-style single-line entry (the 8 metadata-only mappings) — none of
      // these carry a nested flow-map, so a single-line regex is safe here.
      flush()
      const s = trimmed.match(/source:\s*([a-zA-Z0-9_]+)/)
      const t = trimmed.match(/target:\s*([^\s,}]+)/)
      if (!s || !t) throw new Error(`mapping YAML: flow-style entry missing source/target: ${trimmed}`)
      mappings.push({ source: s[1], target: t[1] })
      continue
    }

    if (trimmed.startsWith('- ')) {
      // New block-style entry — flush the previous one.
      flush()
      const rest = trimmed.slice(2)
      if (rest.startsWith('source:')) currentSource = rest.slice('source:'.length).trim()
      continue
    }

    // Continuation line — only capture when the KEY IS THE LINE'S OWN first
    // token. This is what excludes `unit: { source: s, target: s, ... }`:
    // that line's own first token is "unit:", not "source:"/"target:".
    if (trimmed.startsWith('target:')) {
      currentTarget = trimmed.slice('target:'.length).trim()
    } else if (trimmed.startsWith('source:')) {
      currentSource = trimmed.slice('source:'.length).trim()
    }
  }
  flush()

  return { version, mappings }
}

const raw = readFileSync(YAML_PATH, 'utf8')
const { version, mappings } = parseMappingYaml(raw)

describe('mapping-drift: mapper vs mappings/qaf-to-value-stream-mapping.yaml', () => {
  it('parser sanity: extracted exactly 22 distinct source entries', () => {
    expect(mappings).toHaveLength(22)
    expect(new Set(mappings.map((m) => m.source)).size).toBe(22)
  })

  it('parser sanity: the nested unit: {...} flow-maps did not leak spurious entries', () => {
    expect(mappings.some((m) => m.source === 's' || m.source === 'percent')).toBe(false)
  })

  it('MAPPING_VERSION equals the YAML version', () => {
    expect(version).toBe('qvs-1')
    expect(MAPPING_VERSION).toBe(version)
  })

  it('all 22 YAML sources are covered by the mapper (primary, name, sequence, or metadata-only)', () => {
    const mapperSources = new Set<string>(['prozessbezeichnung', 'positionsnummer', ...PRIMARY_FIELD_MAPPINGS.map((m) => m.source), ...METADATA_ONLY_FIELDS])
    expect(mapperSources.size).toBe(22)
    for (const { source } of mappings) {
      expect(mapperSources.has(source), `YAML source "${source}" not covered by the mapper`).toBe(true)
    }
  })

  it('primary YAML targets (not __sequence__/__metadata__) match PRIMARY_FIELD_MAPPINGS exactly', () => {
    // Widened to string/string: `source`/`target` here come from the runtime
    // YAML parse (generic strings), cross-checked against the typed
    // constants — not a compile-time-guaranteed QAFFieldKey/PrimaryTarget.
    const primaryByTarget = new Map<string, string>(PRIMARY_FIELD_MAPPINGS.map((m) => [m.source, m.target]))
    for (const { source, target } of mappings) {
      if (target === '__metadata__' || target === '__sequence__') continue
      if (source === 'prozessbezeichnung') {
        expect(target).toBe('name')
        continue
      }
      expect(primaryByTarget.get(source), `no PRIMARY_FIELD_MAPPINGS entry for YAML source "${source}" -> "${target}"`).toBe(target)
    }
  })

  it('metadata-only YAML entries (target: __metadata__) match METADATA_ONLY_FIELDS exactly', () => {
    const yamlMetadataSources = mappings
      .filter((m) => m.target === '__metadata__')
      .map((m) => m.source)
      .sort()
    expect(yamlMetadataSources).toEqual([...METADATA_ONLY_FIELDS].sort())
  })

  it('the sequence entry (positionsnummer) is not one of the 12 primary value-field mappings', () => {
    expect(mappings.find((m) => m.source === 'positionsnummer')?.target).toBe('__sequence__')
    expect(PRIMARY_FIELD_MAPPINGS.some((m) => m.source === 'positionsnummer')).toBe(false)
  })

  it('runtime: every primary YAML target is actually settable on the mapped VsmNode', () => {
    const row = buildQafRow({
      prozessbezeichnung: 'Drift-Test-Schritt',
      zykluszeit: 1,
      teileProZyklus: 1,
      anzahlMA: 1,
      bezeichnungAnlage: 'x',
      standort: 'x',
      beschaffungswaehrung: 'EUR',
      ausschuss: 1,
      ausschusskosten: 1,
      mss: 1,
      lohnkosten: 1,
      ruestkosten: 1,
      fk: 1,
    })
    const { nodes } = mapQafRowsToVsmNodes([row])
    const node = nodes[0] as unknown as Record<string, unknown>
    for (const { source, target } of mappings) {
      if (target === '__metadata__' || target === '__sequence__') continue
      expect(node[target], `YAML target "${target}" (source "${source}") was not set on the node`).toBeDefined()
    }
  })

  it('runtime: metadata-only fields never appear as a Node field, even when populated', () => {
    const row = buildQafRow({
      prozessbezeichnung: 'Drift-Test-Schritt',
      teilebenennung: 'Fantasieteil X',
      lohnzuschlagssaetze: 12,
      fek: 3,
      rfgk: 4,
      angebotswaehrung: 'USD',
      wechselkurs: 1.1,
      anzahlProAngebotsteil: 5,
      fkAW: 6,
    })
    const { nodes } = mapQafRowsToVsmNodes([row])
    const node = nodes[0] as unknown as Record<string, unknown>
    for (const source of METADATA_ONLY_FIELDS) {
      expect(node[source], `metadata-only field "${source}" leaked onto the node`).toBeUndefined()
    }
  })
})
