// Kanonische Serialisierung und Determinismus (R-17/R-19, Befunde F-11/F-15).

import { describe, it, expect } from 'vitest'
import { canonicalNumber, canonicalizeNumbers, hashableDocument, contentHash } from '../export-canonical'

describe('canonicalNumber (R-17, Befund F-11)', () => {
  it('beseitigt das Fliesskomma-Artefakt einer Einmalzahlung', () => {
    // Der Anlassfall: 0,0004 EUR an einer Millionensumme sind Rauschen, kein Betrag.
    expect(canonicalNumber(5579440.0003921185)).toBe(5579440)
    expect(canonicalNumber(6852390.0003921185)).toBe(6852390)
  })

  it('lässt Stückpreise unangetastet, auch nahe an einer ganzen Zahl', () => {
    // Hier wäre Runden auf ganze Einheiten ein echter Informationsverlust.
    expect(canonicalNumber(2.0004)).toBeCloseTo(2.0004, 6)
    expect(canonicalNumber(0.999)).toBeCloseTo(0.999, 6)
    expect(canonicalNumber(397.9058)).toBeCloseTo(397.9058, 6)
  })

  it('kürzt lange Nachkommaketten auf eine feste Stellenzahl', () => {
    expect(canonicalNumber(0.39518089621859137)).toBe(0.395181)
    expect(canonicalNumber(-1.054999999999998)).toBe(-1.055)
  })

  it('macht aus nicht darstellbaren Werten null statt 0', () => {
    // Ein stillschweigend zu 0 gemachter Fehlwert wäre schlimmer als ein
    // sichtbares „nicht vorhanden".
    expect(canonicalNumber(Number.NaN)).toBeNull()
    expect(canonicalNumber(Number.POSITIVE_INFINITY)).toBeNull()
  })

  it('lässt eine echte 0 eine 0 sein', () => {
    expect(canonicalNumber(0)).toBe(0)
  })
})

describe('canonicalizeNumbers', () => {
  it('greift rekursiv durch Objekte und Listen', () => {
    const doc = {
      a: 5579440.0003921185,
      b: [{ c: 0.39518089621859137 }, 2.0004],
      d: { e: { f: -1.054999999999998 } },
      g: 'text',
      h: null,
      i: true,
    }
    expect(canonicalizeNumbers(doc)).toEqual({
      a: 5579440,
      b: [{ c: 0.395181 }, 2.0004],
      d: { e: { f: -1.055 } },
      g: 'text',
      h: null,
      i: true,
    })
  })

  it('erhält die Reihenfolge der Schlüssel', () => {
    // Spec Kap. 10: feste Schema-Reihenfolge, nicht alphabetisch — das Dokument
    // soll für Menschen lesbar bleiben.
    const out = canonicalizeNumbers({ zuerst: 1, dann: 2, zuletzt: 3 })
    expect(Object.keys(out)).toEqual(['zuerst', 'dann', 'zuletzt'])
  })
})

describe('hashableDocument + contentHash (R-19, Befund F-15)', () => {
  const doc = () => ({
    format: 'supplierpulse-qaf-compare',
    version: 2,
    meta: { partNumber: '123', comparisonId: 'cmp-1', generatedAt: '2026-08-01T00:00:00.000Z', currency: 'EUR' },
    sections: { kpis: { price: 397.9058 } },
  })

  it('lässt die volatilen Felder aus dem gehashten Dokument heraus', () => {
    const h = hashableDocument(doc()) as { meta: Record<string, unknown> }
    expect(h.meta).not.toHaveProperty('comparisonId')
    expect(h.meta).not.toHaveProperty('generatedAt')
    expect(h.meta).toHaveProperty('partNumber') // alles Übrige bleibt
  })

  it('liefert für zwei Läufe über dieselben Eingaben denselben Hash', async () => {
    // Das Abnahmekriterium der Spezifikation.
    const a = await contentHash(doc())
    const b = await contentHash(doc())
    expect(a).toBe(b)
    expect(a).toMatch(/^sha256:[0-9a-f]{64}$/)
  })

  it('ändert den Hash nicht, wenn nur Lauf-ID und Zeitstempel wechseln', async () => {
    const first = doc()
    const second = { ...doc(), meta: { ...doc().meta, comparisonId: 'cmp-2', generatedAt: '2026-12-24T18:00:00.000Z' } }
    expect(await contentHash(first)).toBe(await contentHash(second))
  })

  it('ändert den Hash, sobald sich ein Inhalt ändert', async () => {
    const changed = { ...doc(), sections: { kpis: { price: 410.8689 } } }
    expect(await contentHash(changed)).not.toBe(await contentHash(doc()))
  })

  it('ändert den Hash nicht durch ein blosses Fliesskomma-Artefakt', async () => {
    // Zwei Läufe, die denselben Betrag einmal exakt und einmal mit Artefakt
    // ausrechnen, sind inhaltlich identisch — der Hash muss das aushalten.
    const exact = { ...doc(), sections: { kpis: { oneTime: 5579440 } } }
    const artefact = { ...doc(), sections: { kpis: { oneTime: 5579440.0003921185 } } }
    expect(await contentHash(exact)).toBe(await contentHash(artefact))
  })
})
