import { describe, it, expect } from 'vitest'
import { exportFilename, buildViewModelExport, withContentHash, escapeXmlText } from '../export-utils'

describe('escapeXmlText', () => {
  it('escapes XML-breaking characters in inlined CSS', () => {
    expect(escapeXmlText('a & b < c')).toBe('a &amp; b &lt; c')
    expect(escapeXmlText('url("x?a=1&b=2")')).toContain('&amp;')
    expect(escapeXmlText(']]>rest')).toBe('rest')
  })
})

describe('exportFilename', () => {
  it('builds V11-style names with a sanitized part and date stamp', () => {
    expect(exportFilename('json', '5A846A0', new Date('2026-07-04T10:00:00Z'))).toBe(
      'SupplierPulse_QAF_Compare_Result_5A846A0_2026-07-04.json',
    )
    expect(exportFilename('png', 'A/B 12', new Date('2026-07-04T10:00:00Z'), 'kostenbruecke')).toBe(
      'SupplierPulse_QAF_kostenbruecke_A_B_12_2026-07-04.png',
    )
  })

  it('falls back to QAF when the part number is missing', () => {
    expect(exportFilename('json', null, new Date('2026-07-04T10:00:00Z'))).toContain('_QAF_2026-07-04')
  })
})

describe('buildViewModelExport', () => {
  const vm = buildViewModelExport({
    partNumber: '5A846A0',
    comparisonId: 'cmp-1',
    currency: 'EUR',
    generatedAtIso: '2026-07-04T10:00:00.000Z',
    sections: { kpis: { a: 1 }, bridge: null },
  })

  it('wraps the view models with version + meta (identical to the rendered view)', () => {
    expect(vm.format).toBe('supplierpulse-qaf-compare')
    expect(vm.version).toBe(2)
    expect(vm.meta).toMatchObject({ partNumber: '5A846A0', comparisonId: 'cmp-1', currency: 'EUR' })
    expect(vm.sections).toEqual({ kpis: { a: 1 }, bridge: null })
    // must be JSON-serializable as-is
    expect(() => JSON.stringify(vm)).not.toThrow()
  })

  // V2-Envelope (Spec Kap. 6/13): das Dokument muss selbst sagen, unter welchen
  // Regeln seine Zahlen zu lesen sind — Delta-Richtung, Einheiten, Toleranzen.
  it('nennt Engine und Konventionen im Dokument', () => {
    expect(vm.engine.name).toBe('qaf-compare-core')
    expect(vm.engine.version).toMatch(/^\d+\.\d+\.\d+$/)
    expect(vm.conventions.deltaDefinition).toBe('current_minus_award')
    expect(vm.conventions.signSemantics).toBe('positive_means_more_expensive')
  })

  it('ordnet den Vergleich ein und legt die Grundlage offen (R-23)', () => {
    expect(vm.meta.comparison_type.type).toBeTruthy()
    expect(vm.meta.comparison_type.evidence.length).toBeGreaterThan(0)
  })

  it('rät den Typ nicht, wenn keine Identitätsmerkmale übergeben wurden', () => {
    // Ohne Lieferant und Standort ist „zeitlicher Vergleich" plausibel, aber
    // nicht belegt — das muss am Ergebnis stehen.
    expect(vm.meta.comparison_type.basis).toBe('undetermined')
  })

  it('trägt für jede Sektion ihren Zustand (R-24)', () => {
    expect(Object.keys(vm.section_state).sort()).toEqual(['bridge', 'kpis'])
    expect(vm.section_state.kpis.status).toBe('populated')
    // `bridge: null` heisst „geprüft, nichts gefunden" — nicht wortlos leer.
    expect(vm.section_state.bridge.status).toBe('empty_no_findings')
    expect(vm.section_state.bridge.reasonDe.length).toBeGreaterThan(0)
  })

  it('führt benannte Prüfungen mit Messwert und Grenze (R-14)', () => {
    const ids = vm.validation.checks.map((c) => c.checkId)
    expect(ids).toContain('bridge_closes')
    expect(ids).toContain('determinism_hash_present')
    // Ohne Hash ist der Determinismus nicht belegt — und darf nicht als
    // bestanden gemeldet werden.
    expect(vm.validation.checks.find((c) => c.checkId === 'determinism_hash_present')!.status).toBe('fail')
    expect(vm.validation.determinism.content_hash).toBeNull()
  })

  it('belegt den Determinismus erst mit dem Hash (R-19)', async () => {
    const hashed = await withContentHash(vm)
    expect(hashed.validation.determinism.content_hash).toMatch(/^sha256:[0-9a-f]{64}$/)
    expect(hashed.validation.checks.find((c) => c.checkId === 'determinism_hash_present')!.status).toBe('pass')
    expect(hashed.validation.counts.fail).toBe(vm.validation.counts.fail - 1)
  })

  it('liefert für zwei Aufbauten desselben Inhalts denselben Hash', async () => {
    const build = () =>
      buildViewModelExport({
        partNumber: '5A846A0',
        comparisonId: 'irrelevant',
        currency: 'EUR',
        generatedAtIso: '2026-07-04T10:00:00.000Z',
        sections: { kpis: { a: 1 }, bridge: null },
      })
    const a = await withContentHash(build())
    const b = await withContentHash({ ...build(), meta: { ...build().meta, comparisonId: 'anders' } })
    expect(a.validation.determinism.content_hash).toBe(b.validation.determinism.content_hash)
  })

  it('leitet den Vergleichstyp aus übergebenen Identitätsmerkmalen ab', () => {
    const cross = buildViewModelExport({
      partNumber: '1234567',
      comparisonId: 'cmp-2',
      currency: 'EUR',
      generatedAtIso: '2026-07-04T10:00:00.000Z',
      sections: {},
      identity: {
        partNumberAward: '1234567',
        partNumberCurrent: '1234567',
        supplierAward: 'Lieferant A',
        supplierCurrent: 'Lieferant B',
        siteAward: 'Werk 1',
        siteCurrent: 'Werk 2',
      },
    })
    expect(cross.meta.comparison_type.type).toBe('cross_supplier_same_part')
    expect(cross.meta.comparison_type.basis).toBe('derived_from_identity')
  })
})
