// Verhandlungshebel aus Regeln (Kap. 7.9, R-20; Befund F-16).

import { describe, it, expect } from 'vitest'
import { buildLevers, validateLevers, LEVER_RULES, type Lever, type LeverRule } from '../negotiation-levers'
import { assignDifferenceIds, type DifferenceRecord, type RawDifference } from '../all-differences'

const diff = (over: Partial<RawDifference> = {}): RawDifference => ({
  area: 'SBM',
  kind: 'added',
  formulaFindingKind: null,
  descriptionDe: 'Zweitwerkzeug',
  valueAward: null,
  valueCurrent: 100,
  delta: 100,
  deltaNullReason: null,
  unit: 'EUR',
  cells: [{ fileRole: 'current', sheet: 'SBM-DEVICES-FWZ', cell: 'AP91', formula: null, valueState: 'constant' }],
  relatedDq: [],
  anchorRow: 91,
  ...over,
})

/** Vier Zweitwerkzeuge mit runden Beträgen — die Verhältnisse tragen den Test. */
function zweitwerkzeuge(): DifferenceRecord[] {
  return assignDifferenceIds([
    diff({ anchorRow: 91, delta: 166_300 }),
    diff({ anchorRow: 97, delta: 164_700 }),
    diff({ anchorRow: 98, delta: 250_500 }),
    diff({ anchorRow: 101, delta: 280_000 }),
  ])
}

describe('LEVER_RULES', () => {
  it('führt für jede Regel eine Vorlage mit Schwere', () => {
    for (const [rule, spec] of Object.entries(LEVER_RULES)) {
      expect(spec.template, rule).toBeTruthy()
      expect(spec.severity, rule).toBeTruthy()
    }
  })

  it('nennt einen Betrag nur, wo die Regel eine Einheit hat', () => {
    // Sonst stünde im Text ein Gedankenstrich, wo eine Zahl erwartet wird.
    for (const [rule, spec] of Object.entries(LEVER_RULES)) {
      if (spec.template.includes('{amount}')) expect(spec.unit, rule).not.toBeNull()
    }
  })
})

describe('buildLevers', () => {
  it('rechnet den Betrag aus den Belegen', () => {
    // Der Kern der Datei: Die Zahl steht nicht daneben, sie folgt aus den
    // Differenzen, auf die der Hebel verweist.
    const records = zweitwerkzeuge()
    const [lever] = buildLevers(
      [{ rule: 'second_tool_without_spec', differenceIds: records.map((r) => r.differenceId) }],
      records,
    )
    expect(lever.amount).toBe(861_500)
    expect(lever.textDe).toContain('861.500')
    expect(lever.textDe).toContain('4 Positionen')
  })

  it('setzt jede Stelle der Vorlage ein', () => {
    const records = zweitwerkzeuge()
    for (const rule of Object.keys(LEVER_RULES) as LeverRule[]) {
      const [lever] = buildLevers([{ rule, differenceIds: [records[0].differenceId] }], records)
      expect(lever.textDe, rule).not.toMatch(/\{[a-z_]+\}/)
    }
  })

  it('bildet keinen Hebel, wenn ein Beleg fehlt', () => {
    // Ein Hebel mit geschätztem Betrag ist schlimmer als keiner.
    const records = zweitwerkzeuge()
    expect(
      buildLevers(
        [{ rule: 'second_tool_without_spec', differenceIds: [records[0].differenceId, 'D-SBM-999'] }],
        records,
      ),
    ).toEqual([])
  })

  it('bildet keinen Hebel ohne Belege', () => {
    expect(buildLevers([{ rule: 'spec_removed', differenceIds: [] }], zweitwerkzeuge())).toEqual([])
  })

  it('lässt Belege anderer Einheit aus dem Betrag heraus', () => {
    // EUR und EUR je Stück zu addieren ergäbe eine Zahl, die es nicht gibt.
    const records = assignDifferenceIds([
      diff({ anchorRow: 91, delta: 100_000, unit: 'EUR' }),
      diff({ anchorRow: 88, delta: -0.747, unit: 'EUR_per_piece' }),
    ])
    const [lever] = buildLevers(
      [{ rule: 'second_tool_without_spec', differenceIds: records.map((r) => r.differenceId) }],
      records,
    )
    expect(lever.amount).toBe(100_000)
  })

  it('lässt einen Betrag weg, wo die Regel keinen kennt', () => {
    const records = zweitwerkzeuge()
    const [lever] = buildLevers([{ rule: 'spec_removed', differenceIds: [records[0].differenceId] }], records)
    expect(lever.amount).toBeNull()
    expect(lever.unit).toBeNull()
  })

  it('nimmt eine abweichende Positionszahl entgegen', () => {
    // Ein Befund kann mehrere Zeilen zusammenfassen; dann ist die Zahl der
    // Belege nicht die Zahl der betroffenen Positionen.
    const records = zweitwerkzeuge()
    const [lever] = buildLevers(
      [{ rule: 'spec_removed', differenceIds: [records[0].differenceId], count: 60 }],
      records,
    )
    expect(lever.textDe).toContain('60 Werkzeugpositionen')
  })

  it('vergibt Kennungen in stabiler Reihenfolge', () => {
    const records = zweitwerkzeuge()
    const ids = records.map((r) => r.differenceId)
    const eingabe = [
      { rule: 'spec_removed' as const, differenceIds: [ids[0]] },
      { rule: 'second_tool_without_spec' as const, differenceIds: ids },
    ]
    const key = (ls: Lever[]) => ls.map((l) => `${l.leverId}:${l.rule}`)
    expect(key(buildLevers([...eingabe].reverse(), records))).toEqual(key(buildLevers(eingabe, records)))
  })

  it('führt die Datenqualitäts-Befunde mit', () => {
    const records = zweitwerkzeuge()
    const [lever] = buildLevers(
      [{ rule: 'spec_removed', differenceIds: [records[0].differenceId], dqIds: ['DQ-B-002', 'DQ-B-001'] }],
      records,
    )
    expect(lever.dqIds).toEqual(['DQ-B-001', 'DQ-B-002'])
  })
})

describe('validateLevers', () => {
  const records = zweitwerkzeuge()
  const gueltig = buildLevers(
    [{ rule: 'second_tool_without_spec', differenceIds: records.map((r) => r.differenceId) }],
    records,
  )

  it('meldet nichts für einen belegten Hebel', () => {
    expect(validateLevers(gueltig, records)).toEqual([])
  })

  it('meldet den Betrag, der nicht aus seinen Belegen folgt', () => {
    // Der Fall, für den die Prüfung existiert: eine Zahl, die bei der ersten
    // Rückfrage weg ist.
    const manipuliert = [{ ...gueltig[0], amount: 900_000 }]
    const p = validateLevers(manipuliert, records)
    expect(p.map((x) => x.kind)).toContain('amount_not_backed')
    expect(p[0].detail).toContain('861500')
  })

  it('meldet den Verweis auf eine nicht geführte Differenz', () => {
    const p = validateLevers([{ ...gueltig[0], differenceIds: ['D-SBM-999'], amount: null }], records)
    expect(p.map((x) => x.kind)).toContain('unknown_difference')
  })

  it('meldet den Hebel ohne Beleg', () => {
    const p = validateLevers([{ ...gueltig[0], differenceIds: [] }], records)
    expect(p).toHaveLength(1)
    expect(p[0].kind).toBe('no_evidence')
  })

  it('meldet die nicht ersetzte Stelle in der Textvorlage', () => {
    // Ein Hebel, in dem „{amount}" als Text steht, ist in einer Verhandlung
    // eine Blamage.
    const p = validateLevers([{ ...gueltig[0], textDe: 'Zweitwerkzeuge über {amount} EUR' }], records)
    expect(p.map((x) => x.kind)).toContain('template_placeholder_left')
  })

  it('lässt Rundung auf den Cent durchgehen, aber nicht mehr', () => {
    expect(validateLevers([{ ...gueltig[0], amount: 861_500.004 }], records)).toEqual([])
    expect(validateLevers([{ ...gueltig[0], amount: 861_500.02 }], records).map((x) => x.kind)).toContain(
      'amount_not_backed',
    )
  })
})
